MUnit for Java Programmers: Test doubles

What are test doubles?

A test double is a term used to describe replacing a dependent component of the functionality that is being tested with a dummy version of the real component — similar to a stunt double, only for unit tests. The test double only needs to emulate the behavior of the original dependent component closely enough that the functionality being tested doesn’t detect it’s not using the real component.

Types of test doubles

There are five primary types of test doubles: dummies, stubs, spies, mocks, and fakes. Although each has a defined purpose, in reality, their strict usage is ignored in favor of a simplified approach where mocks are used to replace real components and spies are used to verify the behavior of actual objects.

A mock object verifies that a component is being used correctly by the function under test. If it passes the unit test, it is likely that the real object will be used in the same way. On the other hand, a spy allows you to call the real methods of the underlying object as well as track and verify all interactions.

Mocks

JUnit does not provide a mocking functionality, so developers need to call on a collaborate testing frameworks, such as Mockito. Mockito is an open-source testing framework designed to provide a range of test doubles. A Mockito mock stubs a method call, which means that we can stub a method to return a specific object. Typically, it will be a method that interacts with an external system such as a database. Mocking this type of method call means that the real database is not needed for the unit test. 

Let’s start by looking at how to mock using JUnit/Mockito and then how it is done in MUnit.

Unit testing with Mockito

Consider a service facade that performs simple CRUD actions via a repository instance. The repository is backed by a database. In the test scenario, we want to replace the repository with a mock so that methods on the service facade can be tested.

The service facade manages the CRUD for an employee repository.

Fig 1: The Employee Service facade that uses the EmployeeRepository.

The employee repository interacts with the database. I replaced the actual database configuration with an ArrayList to make the example simpler. 

Fig 2: The EmployeeRepository interacts with the database and will be mocked.

Mockito is annotation-driven, so setting up mocks is simplified by the use of the @Mock annotation.

Fig 3:  Mockito instantiates mocks. 

Now that the EmployeeRepositoy and Employee objects are mocked, each unit test can test the methods of the EmployeeService class without a real method of the EmployeeRepositoy class being called. An example of this is in the following test case.

Fig 4: The case that test the save employee functionality.

The test case calls the saveEmployee() method on the EmployeeService which, in turn, calls the save method in the EmployeeRepository class, although in this case as the EmployeeRepository has been mocked and a return value has been defined using the when() Mockito method, the mocked method is called and not the real method. Therefore, we are safely testing the saveEmployee() method. 

MUnit provides a mocking feature that satisfies the same requirement to mock.

MUnit Mock When

MUnit allows event processors, such as database connectors and flow references, to be mocked. In the example below the employee-service flow calls the persist-employee sub-flow. What MUnit provides the Mock When feature to mockout the call to the sub-flow so that the employee-service flow can be tested.

Fig 5: Employee service flow uses the persist-employee sub-flow.

The MUnit test intercepts calls to the persist-employee sub-flow and replaces the response with a hardcoded payload JSON object:

Fig 6: MUnit mock-when feature mocks a call to the persist-employee sub-flow.

The employee-service flow is tested without a call being made to the external database.

Spy

A Mockito spy functions as a partial mock. Some of the object’s methods can be stubbed, while others are not, allowing invocations of those real methods. So calling a method on a spy will invoke the actual method, unless it has been stubbed.

Let’s go back to the EmployeeService example. The EmployeeRepository has been marked with the @Spy annotation and the save() method has been stubbed. All other methods remain unstubbed. The JUnit test calls the saveEmployee method which delegates the call to the stubbed version and returns the mocked employee object. All other methods remain unstubbed and, if called, would invoke the real method.

Fig 7: The test will call the mocked save() when saveEmployee() is called.

MUnit Spy

MUnit provides a spy feature, however, its implementation is a literal interpretation of what spy means in the sense that it actually allows the test to examine and assert on the state of an object before and after an event processor has been executed.

Using the same flow as in the previous example, the MUnit spy can be used to verify the sata of the payload object before and after the execution of the persist-employee sub-flow.

Fig 8: MUnit spy asserts on state before and after event processor execution.

The test asserts that the payload is NULL before the call to the persist-employee flow and that it has a value afterwards.

Verify call

Method invocations often need to be verified they were called a given number of times. Mockito provides the verify() method that determines the number of times a method has been called. 

Fig 9: The delete() method of the repository should be called when deleteEmployee is called.

MUnit verify call

MUnit provides the same functionality in that a call to an event processor can verify it’s been called a certain number of times or for a greater or lesser number of times

Fig 10: Verify that the Persist Employee flow is called once.

Fail/Exception tests

It’s just as important to test the positive paths as it is to test the negative ones. This is where exception testing plays a role. We need to ensure that when something goes wrong that the correct exception is thrown. Both JUnit and MUnit provide functionality for this kind of testing.

JUnit offers two ways to specify that a given exception is expected via annotations or with an ExpectedException rule. Let’s look at how to use the annotation.

 Fig 11: Specified expected exception in annotation.

The test will pass if the IndexOutOfBoundsException is thrown. Any other exception will fail the test.

MUnit exception testing

MUnit provides that same capability. The expected error is configured on the MUnit test flow itself. 

Fig 12: Define expected error on the MUnit test itself.

The Mule error is defined on the MUnit test flow and, if thrown during test execution, the test will pass. In the code example the MUnit test expects the error type MULE:CONNECTIVITY and in the tests munit:execution body the Raise Error event processor simulates the MULE:CONNECTIVITY error. The test will pass because this error is expected.

Assert Fail

Similar to exception testing, a test failure can assess if a certain behavior does not occur. JUnit provides the Assert.fail() assertion that will make a test case execution fail.

Fig 13: The test is failed if the expected exception is not thrown.

In the example, the test case will fail if the IllegalArgumentException exception is not thrown.

MUnit fail event

MUnit can replicate this behavior with the Fail event processor. 

Fig 14: The test case will fail with the message “Payload is a CAT”

When the Fail event processor is encountered, the test case will fail with the given message.

Conclusion

Test doubles such as mocks and spies are a fundamental part of building a deterministic set of test suites. MUnit fully supports mocking and provides its own literal interpretation of spies which deviates from the behavior in JUnit. For Java developers wanting to learn more Mule integration knowledge will find their current knowledge and skills directly applicable to creating test doubles and exception testing in MUnit.
If you are interested in learning more about MUnit and related DevOps related activities essential for the implement production-ready Mule applications then consider our course Anypoint Platform Development: Production-Ready Development Practices (Mule 4).



We'd love to hear your opinion on this post