Introduction to Test Doubles with Mockito
It is not uncommon to encounter a situation when the System Under Test (SUT) is dependent on a number of collaborators. Let's imagine that we are developing code that emulates an Automatic Teller Machine (ATM). An ATM can be thought of as a box that host various components such as the keyboard, the display, the cash dispenser and so on, as well as it is a controller that orchestrates the work of constituent parts. In addition, it should have a communication link to the bank it belongs, in order to check user's details and the state of her account.
There is a lot of parts that may not be already produced by the time we write the ATM class and also if they were, it could be slow to use real parts in our tests. Moreover, an attempt to communicate with the bank could lead to unpredictable and not repeatable results. Furthermore, it is easier to emulate communication failures with some code than with hardware.
To solve a problem of testing a system which is dependent on other components test doubles are used, a term coined by Gerard Meszaros in his book «xUnit Test Patterns». The author uses an allusion to Stunt Doubles, replacements for real stars, who are specially trained to do some tricks but cannot act themselves. The idea with test doubles is that a real collaborator is replaced with some object with the same API but the object exposes only the behavior necessary for a single test.
There are several frameworks that simplify test doubles creation, but their review is out of scope of this piece. Instead, we'll discuss how to do it with Mockito, a popular framework for testing. For our example we will use JUnit in addition. The code of the example is available here.
First, let's take a look at a method where there is no place for test doubles.
It's a simple method which checks if the user's balance is greater than the amount to be withdrawn, with no collaboration with any parts of the ATM. Such methods are tested as usual, that is something is passed as values and then the result is checked.
Now let's take a look at the method to withdraw some cash.
Firstly, the method displays the menu to input the sum to withdraw. Secondly, it reads the sum from the keypad. Thirdly, the bank service is accessed to obtain the balance of user account. After that there is a check if there is enough money on the account. If it passes, the cash dispenser is checked to have enough notes. If everything is OK, then the account is debited, cash is dispensed and finally, the message about the transaction is shown on the screen. If there are some problems with the amount entered, the error messages are displayed on the screen. Bank service errors handling if left out, it will be discussed later.
Parts not pertaining to our discussion are omitted for brevity. To create test doubles using Mockito one should use a @Mock annotation, which creates a double and initializes the variable it belongs to. In order for this annotation to work, the class should be marked with an annotation @RunWith(MockitoJUnitRunner.class). It is worth mentioning, that it is not necessary that collaborators have implementations, in our example all collaborators are interfaces, though concrete classes can be used with the @Mock annotation. The default behavior of the annotation is that it overrides the behavior of all methods which return a value so that they return null, an empty collection or some default value such as 0 or true, although there is a way to change this behavior and another way of creating test doubles, which entails original method calls as well.
As can be seen from the snippet above, first of all we have to create an instance of our ATM. There five values to be passed to the constructor, not all of which are used to test the method. Some are passed only to fill the place. Such test doubles are called dummies, for example, a depositSlot is a dummy, as it is never used in the method we test, but there is a parameter of the constructor, so it is necessary that the value should be passed.
Imagine, that we would like to test a happy path, that is when there is enough money on the account and in the cash dispenser. To do so we instruct our test doubles to return quantities that pass all checks. For instance, the line Mockito.when(keypad.getAmount()).thenReturn(amount); instructs the keypad to return 1000 when the method getAmount() is called. Static import could be used in our code, but otherwise is done to underscore the methods of Mockito.
In the aforementioned case the test double is a stub, that is the object is instructed to return some value when a particular method is called and it provides some input to our system under test. There are similar actions for the bankService and cashDispenser methods, but there is an important difference as well. There are some checks involving these doubles after the method we test had been called. In particular, we check that the account had been debited exactly once and it had been debited with the correct amount. The same thing is done to cash dispenser, but the fact that the method was called a single time was omitted as it is the default.
The gist of the previous paragraph is that we instructed our test doubles to return desired values in response to method calls, in other words had stubbed them. After that, the doubles had recorded all interactions and we analyzed the interactions afterword. Actually, we checked the indirect outputs of our SUT and made sure that it behaves correctly, that is all operations took place desired number of times an involved desired amount to be withdrawn. A test double with such a behaviour is called a mock and, as can be seen, it includes the functionality of a stab in it.
If there is a problem connecting to the bank, an error message is shown to the user. To test that the correct message is shown in the case of an error the method below can be used.
First, the double is instructed to throw an exception when method getBalance() is called. Then, after invoking of showBalance() on our SUT, we check that the appropriate error message was shown. This trick allows one to check the behavior of the system under test in a situation when one of its collaborator fails.
To conclude our discussion of test doubles and Mockito let's consider another sad path, when there is not enough money on the account. The expected result of the test would be that no cash is dispensed, that the cash dispenser should be engaged in no operations, and the account must not be debited from the account. In other words, we check that our systems behaves in the desired way in certain circumstances. The snippet of code is shown below.
It shares the same traits as the tests above, but after the execution of the ATM's method we check that there were no calls to the methods of cashDispenser, as well as debit method was not called. The latter is done in a different way than for cashDispenser, because there are interactions in which bankService is entangled, that is when we ask for balance. So, we check that method was called zero times with any possible value of the argument, not only the one we asked to withdraw.
To sum up, test doubles play an important role in testing of a system with collaborators, as they allow to isolate the system under test, improve test performance and simulate various conditions. The Mockito framework alleviates a lot of pain in dealing with such constructs. However, it should be noted that the types of test doubles are not limited to those discussed. There actually are two additional — test spy an fake, which are beyond the scope of this piece.
Please take note that there is more to test in the ATM example: the deposit slot was never used and the recipe printer was not even mentioned. Furthermore, there are other systems consisting of multiple parts, such us a coffee machine or a smartphone, which could be used to try test doubles and Mockito or some other framework. And please don't forget, that on the bank's side there is some code that interacts with an ATM via the Internet and uses some DAO to access the database; the latter can also be mocked.
References
- An ATM case study
- xUnit Test Patterns site
- Mocks aren't Stubs by Martin Fowler
- Mockito at GitHub
- Mockito getting started
Comments
Post a Comment