Best Practices For Unit Testing at Revolut

Arsen Gasparyan
Revolut Tech
Published in
3 min readAug 6, 2021

Testing is important. Unit testing is super important, I’m not questioning that. But sometimes, you write some code and you’d love to cover it with tests, but it feels like it would be so much effort. So you avoid writing tests, or you write less than you would like to. Sound familiar?

I get it, but I also value unit tests and I think writing them should be fun. Otherwise, what’s the point? So at some point I started to analyse why writing unit tests feel like such a chore. I thought finding these showstoppers and streamlining them would make writing unit tests fun again. And make me more productive.

I did it. And I’m sharing it with you. Here’s the list of best practices I follow to keep writing unit tests easy.

The Sample Function

Imagine you have a function that takes some business model, let’s say PriceAlert and the current price, and if the current price exceeds the target price the function returns true. You don’t care about all other properties of the PriceAlert, you care only about the target price. But here’s the friction point: you want a price alert with a specific target price but you have to fill all other properties.

To solve that, every business model should be provided with a factory method that can create an instance with no input but, if you want to, you can override some properties. Sound familiar?

As you can see, we provide a default value for every property so if you really don’t care, you can just ask for an instance. In most cases, you want to provide one or two specific values and you can do that, too. Also, keep in mind that every sample function is a pure function, so you shouldn’t use any dynamic values like Date().

The MockFunc Object

The second problem I noticed was the mocks. More specifically, the way we used to set them up. Originally, we used a closure-based generator to create mocks. Something like this:

But the problem with these mocks was that it was super verbose and it cluttered test files really quickly. So again, friction. To solve that problem, I created a simple object to represent the act of mocking, if you will.

So an actual mock looks like this:

The mock object works very simply. It knows what you put in and what you get out of it. But, of course, it doesn’t know how to actually transform input to output. This is where you come in. In most cases, you just provide output no matter what input is. Like this:

The MockFunc also has quite a few handy shortcuts:

The full implementation of the MockFunc can be found here.

The Builders

Another problem I noticed was in the first phase of any unit tests — preparation of the main actor. In this case, it’s more about readability but readability still helps you to write unit tests, because you can read existing ones faster. It counts, guys.

So, we used to prepare everything inside a test case like this:

This simple example preparation takes the same amount of space as the actual test. Not good! But don’t worry, a builder saves the day:

So, now we don’t need to clutter a test case itself and we can focus on the fun part — writing actual unit tests.

It helps to reduce the noise. Also it gives you the ability to share a common setup with other test cases. Let’s say we want to setup the repository with some existing cache, you could create a custom makeRepository and simplify the code above to:

Conclusion

If you look at these best practices separately, they may look minor. But if you combine them, they really help us keep test coverage in check and deliver new updates every week with little to no regression. But we’re not done yet. Please share your best practices, I would love to add them to my collection!

--

--