Connecting...

Pexels Photo 1585711

Avoiding Mocks in ScalaTest by Nicolas A Perez

Pexels Photo 1585711

Mocking is an integral part of the testing process and one question that Software Engineer, Nicolas A Perez is often asked is how does he do mocking when writing tests for Scala applications?

Would you like to know the answer to this question? 

Let's explore some thoughts with Nicolas in his interesting article on how to avoid mocks in ScalaTest.

 

 

 

Many times people have asked me how do I do mocking when writing tests for Scala applications. Since testing is a great deal for us (see Simplified Testing with ScalaTest and Custom Matchers) seems that mocking has to be an integral part of our testing technique, so let’s explore some thoughts on this area.

Let’s start by showing a common problem that occurs to us every single day. Please, notice that the example could be very simplistic, but given a context, it can be extrapolated to everyday realistic problems.

Our problem domain is the one related to Application User Manipulation.

Let’s define some high-level abstractions from the very beginning.

1 trait DB[A, ID] {
2   def store(a: A): ID
3
4   def retrieve(id: ID): Option[A]
5
6   def all(): Seq[ID]
7 }

First, we have an interface that store and retrieve objects from some kind of storage. As you may see, this definition is very abstract, and we need, at some point, specializations of it.

Now, we can define another abstraction closer to our domain.

1 trait UserRepository {
2
3   type UserId = String
4
5   def add(user: User): UserId
6
7   def select(predicate: User => Boolean): Seq[User]
8 }
9 
10 object UserRepository {
11   def apply(db: DB[User, UserId]): UserRepository = new UserRepository {...} // the implementation we are testing
12 }

UserRepository is the abstraction that represents how we are going to manage the users in our application.

At this point, we need to implement UserRepository and it needs a concrete representation of DB to work with.

Let’s start by writing some tests.

1 class UserRepositorySpec 
2   extends FlatSpecs 
3   with Matchers {
4  
5   it should "always add user" in {
6     val user = UserRepository(db).add("Joe")
7  
8     user should haveName "Joe"
9     db.retrieve(user.id) should haveName "Joe"
10   }
11 }

There is nothing unusual about this test. The only thing it does is make sure a user is recoverable once it has been added it. The problem with it is that it needs some instance of DB.

At this point, we can add a mocking lib to our dependencies and start mocking DB; but the question is if that is really necessary.

What about if just implement a version of it we can use in our tests?

1 class InMemDB extends DB[User, String] {
2
3   val map = collection.mutable.Map.empty[String, User]
4
5   override def store(a: User): String = {
6     map.update(a.id, a)
7 
8     a.id
9   }
10
11   override def retrieve(id: String): Option[User] = map.get(id)
12 
13   override def all(): Seq[String] = map.keySet.toSeq
14 }

Now, in our tests, we need to initialize InMemDB before running them.

1 it should "always add user" in {
2  
3   val db = new InMemDb
4   
5   val user = UserRepository(db).add("Joe")
6   
7   user should haveName "Joe"
8   db.retrieve(user.id) should haveName "Joe"
9 }

In some way, we are creating a real implementation of DB which is fine for many cases, but sometimes we want to have more flexibility and in here is where mocking libs shine.

1 it should "always add user" in {
2  
3   val db = mock[DB]
4     .when(_.store("Joe"))
5     .do(_ => "Joe")
6   
7   val user = UserRepository(db).add("Joe")
8   
9   user should haveName "Joe"
10   db.check(_.store(_)).wasCalledWith("Joe")
11 }

In this case, we don’t have to implement DB to use it. We can mock/simulate the behavior of it in the way we want. However, in order to setup the mocking, extra steps are required.

Let’s write another test using this technique.

1 it should "select user with short name" in {
2  
3   val db = mock[DB]
4     .when(_.all())
5     .do(_ => Seq("Joe", "Nicolas", "Ruth", "Doe", "Maria"))
6   
7   val usersWithShortName = UserRepository(db).select(_.name.length <= 3)
8   
9   usersWithShortName should contain ("Joe", "Doe")
10 }

In here we are testing that the predicate (user.name.length ≤ 3) is being executed.

As you can see, it is not hard to set up these mocks, but it could also be another way to do the same without introducing extra dependencies to our tests.

What about if we could write the same test in the following way.

1 it should "select user with short name" in {
2   
3   val db = new DB[User, String] {
4     override def all(): Seq[String] = Seq("Joe", "Nicolas", "Ruth", "Doe", "Maria")
5 
6     override def store(a: User): String = ???
7     override def retrieve(id: String): Option[User] = ???
8   }
9   
10   val usersWithShortName = UserRepository(db).select(_.name.length <= 3)
11 
12   usersWithShortName should contain ("Joe", "Doe")
13 }

We can actually create an instance of DB and implement it inline. The drawback is that we need to have the other functions there as well, even when we are not using them.

We can cheat around of this by creating another trait with default implementation and then using the new one within the tests.

1 trait UserDBForTesting extends DB[User, String] {
2  
3   override def all(): Seq[String] = ???
4
5   override def store(a: User): String = ???
6
7   override def retrieve(id: String): Option[User] = ???
8 }

And now we can rewrite our test as follow.

1 it should "select user with short name" in {
2   
3   val db = new UserDBForTesting {
4     override def all(): Seq[String] = Seq("Joe", "Nicolas", "Ruth", "Doe", "Maria")
5   }
6   
7   val usersWithShortName = UserRepository(db).select(_.name.length <= 3)
8 
9   usersWithShortName should contain ("Joe", "Doe")
10 }

It is important to remember that in here we are not testing DB at all. All that matters here is to be able to successfully test UserRepository.

There is at least one more way to replace DB for something we could use in UserRepository. We could mix in DB with our test class.

1 class UserRepositorySpec 
2   extends FlatSpecs 
3   with Matchers 
4   with DB[User, String] {
5   
6   it should "select user with short name" in {
7    
8     val usersWithShortName = UserRepository(this).select(_.name.length <= 3)
9     
10     usersWithShortName should contain ("Joe", "Doe")
11   }
12     
13   override def all(): Seq[String] = ...
14   override def store(a: User): String = ...
15   override def retrieve(id: String): Option[User] = ...
16 }

In this particular way, we need to implement the DB functions inside our test class which makes it an instance of DB. That is kind of the same we did with UserDBForTesting, but in this case, we don’t need an extra class, and the implementation is exactly what our test needs and nothing else.


 

Let’s look at another example that brings a different way of simulating/replacing implementation by using type classes and context.

1 @typeclass trait Writer[A] {
2   def put(a: A): Boolean
3 }

This 'type class' is an abstract Writer that we could implement with some kind of specialization, in particular for User.

The idea is that we could do the following function calls.

1 implicit val path = "somewhere on the file system"
2
3 val user: User = "Joe"
4 
5 user.put

In order to make this happens, we need to implement Write[User] which is the one that actually writes down the User to disk. Writing a User should never fail.

1 object UserStorage {
2 
3   implicit def toWriter(implicit path: String): Writer[User] = new Writer[User] {
4     override def put(a: User): Boolean = ???
5   }
6 
7 }

The involved tests for Writer[User] can be done in multiple ways, probably integration tests are a good approach to this problem. However, from this layer up, we don’t have to deal with the disk anymore. We can simply replace the implementation of put in our tests.

Suppose we have to write user manager that override user info in some context. The context itself does not have to be fixed.

1 trait UserManager {
2   
3   def replace(users: User*): (UserId, Boolean)
4  
5   ...
6 }

.replace not only writes to disk, but also do other verifications on users.

Let’s see how a test for it looks like.

1 class UserManagerSpec 
2 extends FlatSpec 
3 with Matchers {
4
5   it should "write users" in {
6     val users = ...
7     
8     val results = UserManager().replace(users)
9 
10     results.map(_._1).forAll(userId => user.map(_.id).contains(userId)) should be (true)
11     results.map(_._2).forAll(v => v == true) should be (true)
12   }
13 }

Internally, replace should do at some point.

1 users.map(_.put)

The problem is that for testing UserManager we should not go to disk all at. We could simply replace '.put' calls for testing. A way to do it is by replacing the context that is selected during testing.

1 object TrueTestingUserStorage {
2   
3   implicit def toWriter(implicit path: String): Writer[User] = new Writer[User] {
4     override def put(a: User): Boolean = true // Or false, depending on the test
5   }
6 }

And then we implicitly import TrueTestingUserStorage mocking/simulating the dependency that the class under test (UserManager) has.


 

Going back to the question How do I mock? Well, I normally don’t.

We just saw few different ways of replacing mocks. I don’t mock because I can’t ever remember the specifics of the framework; because people in the team have strong opinions about how to do mocking and each person has different preferences on frameworks. There is too much to consider so that complexity and decision taking is harder to deal with.

I personally find easier to use pure Scala techniques to mimic implementations of test dependencies while keeping small builds and library dependencies.

It looks like mocking makes its way when there is is a lot of state to be managed inside your application components. However, having pure functions and less state within our modules makes easier to test the different part of the application/modules. Sometimes it takes time to embrace pure functional programming, but one of the benefits it brings to the table is the simplicity of the tests around it.

Scala mix in does the job just fine.

Many people struggle with these ideas. Please, share what you think about it and how you do mocking and testing in general. I will be glad to learn more from the Community.​

Notice that we were using some testing techniques and Scala implicit as we discussed in a previous post (see Simplified Testing with ScalaTest and Custom Matchers)'

 

This article was written by Nicolas A Perez and posted originally on hackernoon.com