Avoiding Mocks in ScalaTest

Written by anicolaspp | Published 2017/08/06
Tech Story Tags: testing | scala | functional-programming | big-data | tdd

TLDRvia the TL;DR App

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.

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.

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.

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?

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

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.

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.

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.

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.

And now we can rewrite our test as follow.

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.

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.

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.

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.

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.

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

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

Internally, replace should do at some point.

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.

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)


Published by HackerNoon on 2017/08/06