1. Introduction
In this short article, we’ll showcase the basics of ScalaMock, a Scala native library for test mocking that can also be considered a Mockito alternative.
2. Setup
ScalaMock integrates with ScalaTest and Specs2.
To use ScalaMock with ScalaTest or Specs2, we need to add the dependency to our sbt file:
libraryDependencies += "org.scalamock" %% "scalamock" % "6.0.0" % Test
3. Features
Now that we’ve installed the library let’s describe and demonstrate some of the key features of ScalaMock.
3.1. Argument Matching
There are a few ways to achieve argument matching.
Firstly, we need to create some classes and functions:
// Models
case class Model1(id: Long, name: String)
case class Model2(id: Long, name: String)
// Services
trait UnitService1 {
def doSomething(m1: Model1, m2: Model2): Unit
}
class UnitService2Impl(srv1: UnitService1) {
def doSomething(m1: Model1, m2: Model2): Unit = {
srv1.doSomething(m1, m2)
}
}
With our classes and functions set up, we can now write our test cases with mocks:
"UnitService2Impl" should {
"call Service1 doSomething result" in {
val m1 = Model1(1L, "Jim")
val m2 = Model2(2L, "Timmy")
val mockUnitService1 = mock[UnitService1]
(mockUnitService1.doSomething _).expects(m1, m2)
// (mockUnitService1.doSomething _).expects(m1, *) // * is the AnyMatcher, also succeeds
// (mockUnitService1.doSomething _).expects(where { // functional/predicate matcher, also succeeds
// (_m1: Model1, _m2: Model2) => _m1.id == 1L && _m2.id == 2L
//})
val unitService2 = new UnitService2Impl(mockUnitService1)
unitService2.doSomething(m1, m2)
succeed
}
}
It’s important to note that any of the above matches will succeed in the above example.
Another useful feature is epsilon matching. An EpsilonMatcher will match numbers that are close to a value:
"EpsilonMatcher" should {
case class Model3() {
def numFunc(num: Float): Unit = {}
}
"match close numbers" in {
val mockedModel3 = mock[Model3]
(mockedModel3.numFunc _).expects(~71.5f)
mockedModel3.numFunc(71.50002f) // success
mockedModel3.numFunc(71.502f) // failure
succeed
}
}
3.2. Ordered Verification
Another helpful feature is that we can verify the execution order on a mocked instance. To showcase this feature, we need to add some code to our services:
trait UnitService1 {
def doSomething(m1: Model1, m2: Model2): Unit
def doSomethingElse(m1: Model1, m2: Model2): Unit
}
class UnitService2Impl(srv1: UnitService1) {
def doSomething(m1: Model1, m2: Model2): Unit = {
srv1.doSomething(m1, m2)
}
def doManyThings(m1: Model1, m2: Model2): Unit = {
srv1.doSomething(m1, m2)
srv1.doSomethingElse(m1, m2)
}
def doManyThingsInverted(m1: Model1, m2: Model2): Unit = {
srv1.doSomethingElse(m1, m2)
srv1.doSomething(m1, m2)
}
}
Now, let’s write some test cases with order verification:
"Ordering" should {
"verify that doSomething is called before doSomethingElse" in {
val m1 = Model1(1L, "Jim")
val m2 = Model2(2L, "Timmy")
val mockUnitService1 = mock[UnitService1]
inSequence {
(mockUnitService1.doSomething _).expects(m1, m2)
(mockUnitService1.doSomethingElse _).expects(m1, m2)
}
val unitService2 = new UnitService2Impl(mockUnitService1)
unitService2.doManyThings(m1, m2) // success
// unitService2.doManyThingsInverted(m1, m2) // failure
succeed
}
"verify that doSomething and doSomethingElse are called in any order" in {
val m1 = Model1(1L, "Jim")
val m2 = Model2(2L, "Timmy")
val mockUnitService1 = mock[UnitService1]
inAnyOrder {
(mockUnitService1.doSomething _).expects(m1, m2)
(mockUnitService1.doSomethingElse _).expects(m1, m2)
}
val unitService2 = new UnitService2Impl(mockUnitService1)
// unitService2.doManyThings(m1, m2) // also success
unitService2.doManyThingsInverted(m1, m2) // success
succeed
}
}
3.3. Call Count
Additionally, ScalaMock enables us to check the number of calls on a mocked instance function. The following test case shows how:
"CallCounts" should {
case class Model3() {
def emptyFunc(): Unit = {}
}
"verify the number of calls" in {
val mockedModel3 = mock[Model3]
(mockedModel3.emptyFunc _).expects().once()
// (mockedModel3.emptyFunc _).expects().twice() // exactly 2 times
// (mockedModel3.emptyFunc _).expects().never() // never called
// (mockedModel3.emptyFunc _).expects().repeat(4) // exactly 4 times
(mockedModel3.emptyFunc _).expects().repeat(5 to 12) // between 5 and 12 times
mockedModel3.emptyFunc()
succeed
}
}
3.4. Returning Values
The returning function on mocks allows us to control the returned values of a function. Let’s write a case with the use of returning:
"Returning" should {
case class Model3() {
def getInt(): Int = 3
}
"verify the number of calls" in {
val mockedModel3 = mock[Model3]
(mockedModel3.getInt _).expects().returning(12)
assert(mockedModel3.getInt() === 12)
}
}
3.5. Exception Throwing
If we need to throw an exception during a mock call, we can use the throwing mock function. Let’s see it in action:
"ExceptionThrowing" should {
case class Model3() {
def getInt(): Int = 3
}
"throw exception on mock call" in {
val mockedModel3 = mock[Model3]
(mockedModel3.getInt _).expects().throwing(new RuntimeException("getInt called"))
assertThrows[RuntimeException](mockedModel3.getInt())
}
}
3.6. Call Handlers
Call handlers allow us to compute the returning value of a function based on the given arguments. Let’s write a test case with a call handler:
"CallHandlers" should {
case class Model3() {
def get(num: Int): Int = num - 1
}
"return argument plus 1" in {
val mockedModel3 = mock[Model3]
(mockedModel3.get _).expects(*).onCall((i: Int) => i + 1)
assert(mockedModel3.get(4) === 5)
}
}
3.7. Argument Capture
We can capture arguments with Capture implementations. This functionality can become quite handy when we have no control over the caller.
The next example shows how arguments can be captured with ScalaMock:
"ArgumentCapture" should {
trait OneArg {
def func(arg: Int): Unit
}
"capture arguments" in {
val mockedOneArg = mock[OneArg]
val captor = CaptureOne[Int]()
(mockedOneArg.func _).expects(capture(captor)).atLeastOnce()
mockedOneArg.func(32)
assert(captor.value === 32)
}
}
4. Mocking Styles
In the previous section, we showed various features using the same mocking style. Specifically, we used the expectations-first style. ScalaMock allows us to choose between two styles, expectations-first and record-then-verify. Notably, the latter is more familiar for those who have worked with Mockito.
There’s no strict rule on whether someone should use one over the other or both. Let’s showcase a record-then-verify styled test case:
"MockingStyle" should {
trait MockitoWannabe {
def foo(i: Int): Int
}
"record and then verify" in {
val mockedWannabe = stub[MockitoWannabe]
(mockedWannabe.foo _).when(*).onCall((i: Int) => i * 2)
assert(mockedWannabe.foo(12) === 24)
(mockedWannabe.foo _).verify(12)
}
}
5. Mocking Functions
5.1. Simple Functions
Using mockFunction, we can mock functions similarly to classes or traits. Let’s demonstrate the way to mock a simple function:
"mock simple functions" in {
val mockF = mockFunction[Int, Int]
mockF.expects(*).onCall((i: Int) => i * 2).anyNumberOfTimes()
assert(mockF.apply(1) === 2)
assert(mockF.apply(11) === 22)
}
5.2. Polymorphic Functions
If we need to mock polymorphic functions, we need to specify the type in the expectation declaration:
"PolymorhicFunctions" should {
"be mocked" in {
trait Polymorphic {
def call[A](arg: A): A
}
val mockPolymorphic = mock[Polymorphic]
(mockPolymorphic.call[Int] _).expects(1).onCall((i: Int) => i * 2)
assert(mockPolymorphic.call(1) === 2)
}
}
5.3. Overloaded Functions
Like polymorphic functions, overloaded functions can be mocked by simply declaring the argument types:
"mock overloaded variants" in {
trait Overloader {
def f(i: Int): String
def f(s: String): String
def f(t: (Int, String)): String
}
val mockedOverloader = mock[Overloader]
(mockedOverloader.f(_: Int)).expects(*).onCall((i: Int) => s"Int variant $i")
(mockedOverloader.f(_: String)).expects(*).onCall((i: String) => s"String variant $i")
(mockedOverloader.f(_: (Int, String))).expects(*).onCall((i: (Int, String)) => s"Tuple variant (${i._1}, ${i._2})")
assert(mockedOverloader.f(1) === "Int variant 1")
assert(mockedOverloader.f("str") === "String variant str")
assert(mockedOverloader.f((1, "str")) === "Tuple variant (1, str)")
}
5.4. Curried Functions
Methods with multiple argument lists are mocked by filling each argument list in the expectation call:
"mock curried functions" in {
trait CurryFunc {
def curried(i: Int)(str: String): List[String]
}
val mockedCurryFunc = mock[CurryFunc]
(mockedCurryFunc.curried(_: Int)(_: String)).expects(*, *).onCall((i, str) => Range(0, i).map(num => s"$str-$num").toList)
assert(mockedCurryFunc.curried(2)("myStr") === List("myStr-0", "myStr-1"))
}
5.5. Implicit Arguments
Implicit argument lists should be included in the expectation declarations equally to regular argument lists. In essence, implicit arguments are handled just like any other argument:
"use implicits" in {
trait WithImplicit {
def func(i: Int)(implicit j: BigDecimal): BigDecimal
}
val mockedWithImplicit = mock[WithImplicit]
(mockedWithImplicit.func(_: Int)(_: BigDecimal)).expects(*, *).returning(BigDecimal(41))
implicit val bigD = BigDecimal(12)
assert(mockedWithImplicit.func(0) === BigDecimal(41))
}
6. Mockito Comparison
When presenting a mocking framework, it’s only natural to compare it with Mockito. Although test cases written with the record-then-verify style can yield code very similar to Mockito code, there are some key differences between the two frameworks.
The most significant difference is that Mockito can mock vals, vars, and class members while ScalaMock can’t.
Let’s mock vals, vars, and class members with Mockito in the example below:
"Foo" should {
case class Foo(i: Int) {
val j: Int = 3
val k: Int = 3
}
"mock vars, vals and class members" in {
val mockedFoo = mock[Foo]
when(mockedFoo.i).thenReturn(12)
when(mockedFoo.j).thenReturn(14)
when(mockedFoo.k).thenReturn(15)
assert(mockedFoo.i === 12)
assert(mockedFoo.j === 14)
assert(mockedFoo.k === 15)
}
}
ScalaMock uses macros to override functions in contrast with Mockito, which uses reflection. In Scala, we can’t override val, lazy val or var with functions, hence the ScalaMock limitation.
On the contrary, ScalaMock syntax for mocking functions needs less code and seems more intuitive. Let’s see a comparison between the two:
// Mockito
"MockitoBoilerplate" should {
class Foo() {
def call(f: Int => String, i: Int): String = f.apply(i)
}
"argument matcher for function should work" in {
val mockedFoo = mock[Foo]
when(mockedFoo.call(Matchers.any[Int => String].apply, Matchers.anyInt())).thenAnswer(new Answer[String] {
override def answer(invocation: InvocationOnMock): String = {
val intArg = invocation.getArgumentAt(1, classOf[Int])
Range(0, intArg).mkString(",")
}
})
assert(mockedFoo.call(_ => "bla", 3) === "0,1,2")
}
}
// ScalaMock
"for mockito comparison" in {
trait Foo {
def call(f: Int => String, i: Int): String
}
val mockedFoo = mock[Foo]
(mockedFoo.call _).expects(*, *).onCall((f: Int => String, i: Int) => Range(0, i).mkString(","))
assert(mockedFoo.call(_ => "bla", 3) === "0,1,2")
}
7. Conclusion
In this article, we covered basic and advanced mocking scenarios with ScalaMock. Furthermore, we made a brief and pragmatic comparison with Mockito, the most used mocking framework.
As always, the code of the above examples is available over on GitHub.