Scala - Mocking Techniques


Mocking is an important technique in software testing. You can simulate and control the behavior of dependencies and external systems. It is used in testing code that interacts with external resources, like databases, web services, and third-party libraries. You can isolate the code under test and focus on its core logic without affecting the availability and state of external dependencies.

Mockito

Mockito is the most used mocking framework in the JVM ecosystem. It is simple and flexible. You can create mock objects, with behavior, and verifying interactions. But it also has some challenges, like, higher-order functions, traits, and type inference. We also have a mockito-scala library to resolve the above issues.

Getting Started with Mockito in Scala

You need to add the required dependencies to your build tool to get started with Mockito in Scala. You need to add the following to your build.sbt for SBT -

libraryDependencies ++= Seq(
  "org.scalatestplus" %% "mockito-5-10" % "3.2.18.0" % Test,
  "org.scalatest" %% "scalatest" % "3.2.18" % Test
)

If you are using Maven, then you need to add this dependency -

<dependency>
    <groupId>org.scalatestplus</groupId>
    <artifactId>mockito-5-12_3</artifactId>
    <version>3.2.19.0</version>
    <scope>test</scope>
</dependency>

Note that your project should use JDK 11 and higher because Mockito 5 requires it. If you are on JDK 8, then you will need to use mockito-4-11.

Creating and Using Mock Objects

Basic Mocking

You can create mock objects with ease using Mockito. You can create mocks and define their behavior using MockitoSugar -

val dao = mock[InventoryTransactionDao]

Mocking Methods

You can use the when().thenReturn() construct to define the behavior of a mock object method. For example -

when(dao.getAll()).thenReturn(Future.successful(Seq.empty[InventoryTransaction]))

You can also handle exceptions by using thenThrow -

when(dao.getAll()).thenThrow(new RuntimeException("Database error"))

Mocking Methods with Parameters

You can specify the behavior based on the input arguments for methods with parameters -

val txn = InventoryTransaction(1, "item1")
when(dao.saveAsync(txn)).thenReturn(Future.successful(txn))

If the method takes any parameter of a certain type, you need to use any -

when(dao.saveAsync(any[InventoryTransaction])).thenReturn(Future.successful(txn))

Verifying Method Calls

You can verify interactions with your mock objects using the verify() method -

verify(mockProducer, times(1)).publish(any[InventoryTransaction])

For more precise verification, like checking that a method was never called -

verify(mockProducer, times(0)).publish(any[InventoryTransaction])

Or using the never helper method -

verify(mockProducer, never).publish(any[InventoryTransaction])

Argument Captors

Mockito has ArgumentCaptor to capture arguments passed to mock methods -

val refCapture = ArgumentCaptor.forClass(classOf[String])
verify(mockLogger).logTime(refCapture.capture(), any[LocalDateTime])
refCapture.getValue shouldBe "expectedValue"

Spies

Sometimes you need to use real methods for part of a mock object while stubbing others. You can achieve it using spies -

val dao = spy(new InventoryTransactionDaoImpl)
when(dao.saveAsync(txn)).thenReturn(Future.successful(txn))

Partial Mocks

You need only specific methods are mocked while the rest behave normally for partial mocks -

val service = mock[OtherService]
when(service.login("Anne", "xx")) thenReturn Some(User(222, "AA"))

Mockito-Scala Enhancements

Idiomatic Syntax

Mockito-Scala provides an idiomatic syntax for API more Scala-like. For example -

aMock.bar shouldReturn "mocked!"
aMock.baz(*) shouldReturn "mocked!"

Improved Matchers

Mockito-Scala has enhanced matchers that are more consistent and readable -

aMock.method(n > 4.99) was called
aMock.method(n <= 5) was called

Default Answers

Mockito-Scala has better defaults for handling non-stubbed method calls, using smart nulls and empty values -

implicit val defaultAnswer: DefaultAnswer = ReturnsEmptyValues orElse ReturnsSmartNulls

Cats and Scalaz Integration

You can integrate mockito-scala to work with these libraries for projects using Cats and Scalaz -

val aMock = mock[Foo]
whenF(aMock.returnsOption(*)) thenReturn "mocked!"

Testing Strategies

Using Traits for Setup

You can use traits for common setup. It reduces boilerplate and improves test readability.

trait Setup {
  val myMock = mock[SomeService]
}

class MySpec extends WordSpec with Setup {
  "A test" should {
    "do something" in {
      myMock.someMethod returns "result"
      // test logic
    }
  }
}

Example

You should have these dependencies in your build.sbt file -

import Dependencies._

ThisBuild / scalaVersion := "2.13.14"

lazy val root = (project in file("."))
  .settings(
    name := "Demo",
    version := "0.1",
    libraryDependencies ++= Seq(
      "org.scalatest" %% "scalatest" % "3.2.18" % Test,
      "org.scalatestplus" %% "mockito-3-4" % "3.2.10.0" % Test
    )
  )

Now, you can define MySpec.scala under src/test/scala folder -

import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.matchers.should.Matchers
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._

trait SomeService {
  def someMethod: String
}

trait Setup extends MockitoSugar {
  val myMock = mock[SomeService]
}

class MySpec extends AnyWordSpec with Matchers with Setup {
  "A test" should {
    "do something" in {
      // Define the behavior of the mock
      when(myMock.someMethod).thenReturn("result")

      // Call the method and assert the result
      myMock.someMethod shouldEqual "result"
     
      // Additional test logic can be added here
    }
  }
}

Note that your project SomeService is defined somewhere in your project. It can be a simple trait (or class) as shown in the test example.

Commands

Now, you can use these commands to compile and run test -

sbt compile
sbt test

Output

[info] MySpec:
[info] A test                                                         
[info] - should do something                                          
[info] Run completed in 774 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: 5 s, completed 07-Aug-2024, 5:42:31 pm 

Handling Mocked Objects in Sessions

Mockito-Scala has MockitoScalaSession to manage mock sessions and to verify proper behavior -

MockitoScalaSession().run {
  val foo = mock[Foo]
  when(foo.bar("test")) thenReturn "mocked"
  foo.bar("test") shouldBe "mocked"
}

Best Practices

Mocking Traits and Classes

You can use the appropriate strategy based on your test requirements when mocking traits and classes. If you need to mock a method in a trait and use it in a class extending that trait -

val service = mock[OtherService]
when(service.login("Anne", "xx")) thenReturn Some(User(222, "AA"))
service.getId should be(222)

Partial Mocks and Composition

You can use composition over inheritance. You can inject dependencies through the constructor to facilitate mocking -

class OtherService(loginService: LoginService) {
  def getId: Int = {
    loginService.login("Anne", "xx").get.id
  }
}

val fakeService = mock[LoginService]
when(fakeService.login("Anne", "xx")) thenReturn Some(User(222, "AA"))
val other = new OtherService(fakeService)
other.getId should be(222)

Using the IdiomaticMockito Trait

You can use the IdiomaticMockito trait -

class MyTest extends WordSpec with Matchers with IdiomaticMockito {
  "A service" should {
    "return a mocked value" in {
      val service = mock[Service]
      service.method shouldReturn "mocked!"
      service.method shouldBe "mocked!"
    }
  }
}

Advanced Argument Matching

You can use argThat and custom matchers for argument matching -

when(service.method(argThat(new ArgumentMatcher[CustomType] {
  def matches(argument: CustomType): Boolean = argument.field == "expected"
}))).thenReturn("mocked!")

Or using Scala syntax -

when(service.method(argMatching {
  case CustomType("expected") => true
  case _ => false
})).thenReturn("mocked!")

Handling By-Name Arguments

Mockito-Scala has full support for by-name arguments. So you can stub methods with such parameters -

class Foo {
  def byNameMethod(x: => String): String = x
}

val foo = mock[Foo]
when(foo.byNameMethod(any[String])) thenReturn "mocked!"
foo.byNameMethod("test") shouldBe "mocked!"

Mocking Scala Objects

Mockito-scala is used to mock Scala object methods -

object FooObject {
  def method: String = "not mocked!"
}

withObjectMocked[FooObject.type] {
  FooObject.method returns "mocked!"
  FooObject.method shouldBe "mocked!"
}
FooObject.method shouldBe "not mocked!"