Use Mocks in Testing? Choose the Lesser Evil!
Here, I explain best practices around mocking and why it might be better to avoid mocking in order to have real code quality.
Join the DZone community and get the full member experience.
Join For FreeMocking Test Methodology
The key idea of mocking is to replace real code (e.g. calls to a database or service) with artificial functionality that has the same return type. There are different approaches to this practice. Here, I explain best practices and why it might be better to avoid mocking to have real code quality.
User Service — Example to Be Tested With Mocks
Let's write a simple application that fetches users from HTTP service.
xxxxxxxxxx
public Optional<User> createUser(String name) {
try {
//Preparation of request: auth, body and media type of request
HttpHeaders headers = new HttpHeaders();
headers.setContentType(APPLICATION_JSON);
headers.set("auth", "mySecretKey");
//Specifying body
HttpEntity<String> request =
new HttpEntity<String>("{\"name\":" + name + "}", headers);
//Making request with and deserializing response into User object
User newUser = (new RestTemplate()).postForEntity("/server/users", request, User.class).getBody();
newUser.setIssueDate(Date.now());
return Optional.of(newUser);
} catch (Exception e) {
System.out.println("Something went wrong! : " + e.getMessage());
Return Optional.empty();
}
}
class User {
private Integer id;
private String name;
private Date issueDate;
}
Implementation Details
The method, createUser
, basically does 4 things:
- Preparation of request: authentication, preparing body and media type of request (line 4-5 corresponding to 1, 2, 3 step on the diagram.
- Making requests to the server with a given URL (line 11 or step 4).
- Receiving the response and deserializing it into an object. (line 11- yes the same line! and step 9 on diagram).
- Setting the current date to the created user (line 12, or step 10).
The remaining actions are done on the server-side and hidden from the application:
1. Server check that body has JSON type and correct auth key (step 5,6).
2. Make business logic (create a user) (step 7).
3. Generate a response with a new user id (step 8).
Possible Problems During Testing
If we decide to write a unit test we might face the next problems:
- If we execute a unit test, we just can't call the external service.
- Even if we decided to have an integrated test — a real service will have limitations (e.g. can't be executed in internal network where we can run tests).
- Tightly coupled configuration with real service (e.g. we need to reset the state of the service on each test run).
Creating a Mock in Order to Solve All Mentioned Problems
There are a set of engines that can help us substitute a real call with a fake one: Mockito, Powermock, Spock (Groovy-based but compatible with Java), etc.
But, generally there are at least three ways to do this:
First Way: Write Your Own Mock
For existing APIs, you can just extend and implement you own version. In the case when there is no API but server (e.g. an HTTP server) — you can create your own server (reinventing the wheel). Often, such a solution is time consuming but predictable and straight forward.
Second Way: Use an Engine Like Mockito/PowerMock That Let's You Change Your Code During Runtime
Such a solution essentially changes your code to another version. During class-loading, the engine replaces specified calls with the one you declare before the test. This is the most dangerous but still popular practice. The whole idea of this article is to recommend you to avoid it. But let's see how it might look like for our case:
xxxxxxxxxx
public void createUserTest() {
//Mocking part
RestTemplate template = mock(RestTemplate.class);
User mockedUser = new User(123, "John Smith");
when(template.postForEntity(String.class, HttpEntity.class, User.class)).thenReturn(mockedUser);
//Call to service and asserting part
User actualUser = createUser("John Smith");
assertEquals(actualUser.getId(), mockedUser.getId());
assertEquals(actualUser.getName(), mockedUser.getName());
assertNotNull(actualUser.getIssueDate());
}
It creates an impression that the function is well tested, but practically, it asserts that the returned object has the correct type and that the data is properly set:
This test might be even improved. PowerMock provides features like spy or verify that assert that when parameters passed during the service call have a specific type/value. But it will make code extremely coupled and will multiply size of the test by at least 2-3 times. Also each change in every line of code would require rewriting 2-3 lines of test.
Third Way (Compromise Between Real Server and Artificial Mock): Using Mock Engines That Reproduce Similar To HTTP Server Behavior
For some (not all) protocols or frameworks, you might find mock engines that behave like a real one. For example, for HTTP servers, there is the Spring engine, MockServerClient. Such a solution only requires the configuration of the server behavior. So, you configure only an external service, not an internal implementation.
public void testCreateUser(){
//Mocking part
User mockedUser = new User(12, "John Smith");
new MockServerClient("localhost", 8080)
.when(request()
.withMethod("POST")
.withPath("/server/users")
.withHeader("Content-type", "application/json")
.withHeader("auth", "mySecretKey")
.withBody(exact("{name: '" + mockedUser.getName() + "', id: " + mockedUser.getId() + "}")),
exactly(1))
.respond(
response()
.withStatusCode(201)
.withBody("{ name: 'Joth Smith', id: ' }");
//Asserting part and call to service
User actualUser = createUser("John Smith");
assertEquals(actualUser.getId(), userMockId);
assertEquals(actualUser.getName(), mockedUser.getName());
assertNotNull(actualUser.getIssueDate());
}
Such a solution declares the behavior of the HTTP server, and it's not tightly coupled with the implementation (and it's great!). So, even when you change your implementation, you don't need to change your test! Let's see what steps are covered by this solution:
As you can see, even with that engine, you reproduce the real server, but this approach is far better than the Mokito artificial solution. Unfortunately, not all protocols or frameworks provide corresponding mock engines, and from time to time, there is only one option — to create your own. (Yes, you just create your own embedded HTTP server and declare how it has to respond to all requests).
Example of mocking engines that might help you to write better mock:
For Database — DbUnit helps you to prepare SQL schema with data inside.
For Java Messaging — RabbitMq helps you to mock messaging services.
Instead of Conclusion
The main reason of using mocks is the requirement to write an isolated test. It's often difficult to find a suitable mock engine that reproduces similar behavior to real services. In that case, you have no option but to write your own mock or to use an artificial mock like Mokito or Powermock. But, I wouldn't recommend you to have an isolated test. I strongly recommend to write real end-to-end testing and have real environments. Here's the list of advantages in such an approach:
- Tests and implementation are decoupled so there is no need to change a test if the implementation of a feature has been changed.
- Tests give you real quality and finds scenarios that can't be predicted in mocks.
- Tests are smaller, as they require smaller initialization (or don't require it at all).
Opinions expressed by DZone contributors are their own.
Comments