1. Introduction
Mocking is a process used in software testing to test code that depends on external resources. This helps us to independently test the core logic without worrying about the external APIs. Mockito is one of the most popular mocking frameworks in the JVM. In this tutorial, let’s look at different ways in which we can use Mockito with ScalaTest to test our Scala code.
2. Setup
First, let’s add the required dependencies to build.sbt:
libraryDependencies ++= Seq(
"org.scalatestplus" %% "mockito-5-10" % "3.2.18.0" % Test,
"org.scalatest" %% "scalatest" % "3.2.18" % Test
)
ScalaTestPlus is a collection of testing libraries that are integrated with ScalaTest. For using the Mockito version 5.10, we need to use the mockito-5-10 dependency.
3. Creating a Mock Object
ScalaTestPlus-Mockito provides a utility trait MockitoSugar, which helps to easily create mocks. Let’s say we have a service class, InventoryService, that contains a dependency on the InventoryTransactionDao. After the trait is mixed in with the test class, we can create the mock object for the Dao:
val dao = mock[InventoryTransactionDao]
4. Using Mock Objects in Tests
Let’s look at different scenarios where mock objects are used. For this, we’ll first create a service that uses external dependencies:
class InventoryService(
dao: InventoryTransactionDao,
kafkaProducer: KafkaProducer,
logger: Logger
) {
// different methods are implemented here
}
4.1. Mocking a No-Argument Method
To test the service methods, we need to mock the Dao methods. We can do that by using the when().thenReturn() contract:
val txns: Seq[InventoryTransaction] = Nil
when(dao.getAll()).thenReturn(Future.successful(txns))
Whenever the method getAll from the Dao is invoked, Mockito will return the predefined value, txns. We can use thenThrow() instead of thenReturn() to test a method that might throw some exceptions.
4.2. Mocking a Method With Parameters
To mock a method with parameters, we can use the same approach from the previous section. If the parameter matches with the when() condition, then the mock value will be returned:
val txn: InventoryTransaction = _
when(dao.saveAsync(txn)).thenReturn(Future.successful(txn))
If the saveAsync() method is invoked with another InventoryTransaction object that is not equal to txn, the mocking will not be invoked. To return the same value for any object, we can use the any() method from Mockito:
when(dao.saveAsync(any[InventoryTransaction])).thenReturn(Future.successful(txn))
4.3. Verifying Execution of a Method
In some cases, we need to verify how many times a method is invoked within our test method. For instance, after saving the object into the database, we might need to invoke some other API, depending on certain conditions. We can test such a scenario by using verify():
val mockProducer = mock[KafkaProducer]
val service = new InventoryService(dao, mockProducer)
verify(mockProducer, times(1)).publish(any[InventoryTransaction])
This verifies if the mockProducer.publish() method is called exactly once. Similarly, we can also check if a method is not invoked by using times(0):
verify(mockProducer, times(0)).publish(any[InventoryTransaction])
Mockito provides another helper method, never(), that can be used instead of times(0): verify(mockProducer, never).publish(any[InventoryTransaction])
4.4. Using ArgumentCaptor to Verify Method Arguments
We can use ArgumentCapture to get the values that are applied while invoking a method. This helps to assert that some methods are invoked with expected values. Assume that we have a Logger that saves results to some audit logs for specific scenarios, and this is used from within the save() method of the service:
trait Logger {
def logTime(txnRef: String, dateTime: LocalDateTime): Unit
}
Let’s look at how to define the ArgumentCaptor for logTime():
val refCapture = ArgumentCaptor.forClass(classOf[String])
val refLocalDateTime = ArgumentCaptor.forClass(classOf[LocalDateTime])
Next, while defining the verify() method, we need to apply the previously created captors:
verify(mockLogger, times(1)).logTime(refCapture.capture(), refLocalDateTime.capture())
Now, we can access the argument values for the invoked method as:
refCapture.getValue shouldBe txn.txnRef
refLocalDateTime.getValue shouldBe txn.created
5. Using Spy
If we use a mock object, then all the methods in the class/trait should be properly mocked using the when().then() contract. But in some cases, we need to invoke the actual implementation for a method while other methods are mocked. In such cases, we can use Spy instead of Mock. Let’s look at how we can define and use a spy. Assume that before an object is saved to the database, it’s authenticated. In our test, we want to verify if the authentication is also working as expected. So, we only need to mock the database save, while using the real implementation for authentication:
val dao = spy(classOf[InventoryTransactionDaoImpl])
when(dao.saveAsync(txn)).thenReturn(Future.successful(txn))
val result = service.saveWithAuth(txn, 101)
Note that generally, it’s not suggested to use Spy, since partial mocking is not considered good. But in rare cases or in legacy code where refactoring is not easy, Spy can be used.
6. Conclusion
In this article, we used Mockito with ScalaTest in a Scala codebase. As always, the sample code used here is available over on GitHub.