ScalaMock for Effective Unit Testing


ScalaMock is a mocking framework for Scala applications. You can simulate external dependencies and isolate the unit under test. You can create mock objects, set expectations on their interactions. You can also verify that your code behaves as expected when interacting with these simulated dependencies.

You mock dependencies that involve network communication and file I/O. You can replace these real dependencies with mocks that simulate their behavior for faster tests, more reliable, and easier to write.

Setup

You need to add the following dependency to your build.sbt file to start using ScalaMock -

libraryDependencies += "org.scalamock" %% "scalamock" % "6.0.0" % Test

Now, you can import the required classes in your test suite file -

import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec

Argument Matching

ScalaMock provides various options for argument matching, like, exact, wildcard, and predicate matching.

Exact Match -

(mockObject.method _).expects(arg1, arg2)
Wildcard Match:
(mockObject.method _).expects(*)

Predicate Match -

(mockObject.method _).expects(where { arg => arg > 5 })

Epsilon Matching

ScalaMock supports epsilon matching to account for precision issues for floating-point comparisons -

(mockObject.method _).expects(~42.0)

Ordered Verification

You can verify the execution order of mocked methods for correct sequence of operations.

In Sequence -

inSequence {
  (mockObject.method1 _).expects()
  (mockObject.method2 _).expects()
}

In Any Order -

inAnyOrder {
  (mockObject.method1 _).expects()
  (mockObject.method2 _).expects()
}

Call Count Verification

You can check the number of calls to a mocked method to match expectations.

Exact Number of Calls -

(mockObject.method _).expects().twice()

Range of Calls -

(mockObject.method _).expects().repeat(3 to 5)

Returning Values and Exception Throwing

You can control the return values and exceptions of mocked methods for more precise testing.

Returning Values -

(mockObject.method _).expects().returning(value)

Throwing Exceptions -

(mockObject.method _).expects().throwing(new Exception("Error"))

Call Handlers

You can compute the return value based on the given arguments using call handlers -

(mockObject.method _).expects(*).onCall((arg: Int) => arg + 1)

Argument Capture

You can capture arguments passed to mocked methods for later verification. It is used for more detailed assertions -

val captor = CaptureOne[Int]()
(mockObject.method _).expects(capture(captor))

Mocking Styles

There are two primary mocking styles in ScalaMock: expectations-first and record-then-verify.

Expectations-First -

val mockObject = mock[Service]
(mockObject.method _).expects(arg).returning(result)

Record-Then-Verify -

val mockObject = stub[Service]
(mockObject.method _).when(arg).returns(result)

Example

Consider this example to mock a Formatter trait in a Greetings object to test interactions using ScalaMock. We set up a mock for the Formatter trait and verified that the sayHello method in the Greetings object behaves as expected.

You must have this build.sbt file with following dependencies -

import Dependencies._

ThisBuild / scalaVersion := "2.13.14"

lazy val root = (project in file("."))
  .settings(
    name := "Demo",
    version := "0.1",
    libraryDependencies ++= Seq(
      "org.scalamock" %% "scalamock" % "6.0.0" % Test,
      "org.scalatest" %% "scalatest" % "3.2.10" % Test
    )
  )

Now, you need to define Greetings.scala under src/main/scala with this code -

trait Formatter {
  def format(s: String): String
}

object Greetings {
  def sayHello(name: String, formatter: Formatter): Unit = {
    println(formatter.format(name))
  }
}

Now, you need to define GreetingsTest.scala under src/test/scala with this code -

import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec

class GreetingsTest extends AnyFlatSpec with MockFactory {
  "sayHello" should "format the name correctly" in {
    val mockFormatter = mock[Formatter]
    (mockFormatter.format _).expects("Mr Bond").returning("Ah, Mr Bond. I've been expecting you").once()

    Greetings.sayHello("Mr Bond", mockFormatter)
  }
}

Commands

You can clean and compile using this command -

sbt clean compile

Now you execute the tests using the following command -

sbt test

Output

Ah, Mr Bond. I've been expecting you
[info] GreetingsTest:
[info] sayHello                                                       
[info] - should format the name correctly                             
[info] Run completed in 271 milliseconds.
[info] Total number of tests run: 1                                   
[info] Suites: completed 1, aborted 0                                 
[info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 
[info] All tests passed.                                              
[success] Total time: 4 s, completed 07-Aug-2024, 12:04:25 pm 

Mocking Higher-Order Functions

You can challenge Mocking higher-order functions due to function reference equality. You can use predicate matching to overcome this limitation.

(mockObject.method(_: () => String)).expects(where { f => f() == "expected" }).returns("result")

Integration with Other Testing Frameworks

There are various testing frameworks to integrate with ScalaMock, like ScalaTest and Specs2. You can use AsyncMockFactory for ScalaTest async specs.

Mocking in Scala 3

ScalaMock supports Scala 3 with some migration notes -

  • Use type annotations for methods with by-name parameters.
  • Mocking of non-abstract Java classes requires workarounds.

Mocking Functions

Simple Functions

You can mock simple functions similarly to classes and traits using mockFunction -

val mockF = mockFunction[Int, Int]
mockF.expects(*).onCall((i: Int) => i * 2).anyNumberOfTimes()

assert(mockF.apply(1) === 2)
assert(mockF.apply(11) === 22)

Polymorphic Functions

You can mock polymorphic functions by specifying the type in the expectation declaration -

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)

Overloaded Functions

You can mock overloaded functions by declaring the argument types explicitly -

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)")

Curried Functions

You can mock curried functions by filling each argument list in the expectation call -

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"))

Comparing ScalaMock with Mockito

Both ScalaMock and Mockito are popular mocking frameworks. But these have some differences as given below -

  • Mocking vals, vars, and class members: Mockito can mock these. Whereas ScalaMock cannot.
  • Syntax: ScalaMock may require less boilerplate and is more idiomatic for Scala.

Mockito

class Foo {
  def call(f: Int => String, i: Int): String = f(i)
}

val mockFoo = mock[Foo]
when(mockFoo.call(any(), anyInt())).thenAnswer { invocation =>
  val intArg = invocation.getArgument(1, classOf[Int])
  (0 until intArg).mkString(",")
}

assert(mockFoo.call(_ => "bla", 3) == "0,1,2")

ScalaMock

trait Foo {
  def call(f: Int => String, i: Int): String
}

val mockFoo = mock[Foo]
(mockFoo.call _).expects(*, *).onCall((f: Int => String, i: Int) => (0 until i).mkString(","))
assert(mockFoo.call(_ => "bla", 3) == "0,1,2")