DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Effective Engineering Feedback: Software Testing
  • The LLM Selection War Story: Part 2 - The Six LLM Failure Archetypes That Will Wreck Your Production System
  • Agentic Development: My Invisible Dev Team
  • Clean Code in the Age of Copilot: Why Semantics Matter More Than Ever

Trending

  • Why Your DLP Policies Fall Short the Moment AI Agents Enter the Picture
  • RAG Is Not Enough: Advanced Retrieval Architectures Using Vertex AI Search on GCP
  • Microservices: Externalized Configuration
  • Dear Micromanager: Your Distrust Has a Job; It’s Just Not the One You’re Doing
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Use Mocks in Testing? Choose the Lesser Evil!

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.

By 
Dmitry Egorov user avatar
Dmitry Egorov
DZone Core CORE ·
Updated Oct. 22, 20 · Opinion
Likes (11)
Comment
Save
Tweet
Share
27.0K Views

Join the DZone community and get the full member experience.

Join For Free

Mocking 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. 

Java
 




xxxxxxxxxx
1
25


 
1
public Optional<User> createUser(String name) {
2
        try {
3
            //Preparation of request: auth, body and media type of request
4
            HttpHeaders headers = new HttpHeaders();
5
            headers.setContentType(APPLICATION_JSON);
6
            headers.set("auth", "mySecretKey");
7
            //Specifying body
8
            HttpEntity<String> request =
9
              new HttpEntity<String>("{\"name\":" + name + "}", headers);
10
            //Making request with and deserializing response into User object
11
            User newUser = (new RestTemplate()).postForEntity("/server/users",                                                      request, User.class).getBody();
12
            newUser.setIssueDate(Date.now());
13
            return Optional.of(newUser);
14
        } catch (Exception e) {
15
            System.out.println("Something went wrong! : " + e.getMessage());
16
            Return Optional.empty();
17
        }
18
}
19

          
20
@Data
21
class User {
22
    private Integer id;
23
    private String name;
24
    private Date issueDate;
25
}



Implementation Details

The method, createUser, basically does 4 things: 

  1. Preparation of request: authentication, preparing body and media type of request (line 4-5 corresponding to 1, 2, 3 step on the diagram. 
  2. Making requests to the server with a given URL (line 11 or step 4).
  3. Receiving the response and deserializing it into an object.  (line 11- yes the same line! and step 9 on diagram).
  4. 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).

createUser workflow

Possible Problems During Testing

If we decide to write a unit test we might face the next problems:

  1. If we execute a unit test, we just can't call the external service.
  2. 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).
  3. 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: 

Java
 




xxxxxxxxxx
1
12


 
1
@Test
2
public void createUserTest() {
3
  //Mocking part
4
  RestTemplate template = mock(RestTemplate.class);
5
  User mockedUser = new User(123, "John Smith");
6
  when(template.postForEntity(String.class, HttpEntity.class, User.class)).thenReturn(mockedUser);
7
  //Call to service and asserting part
8
  User actualUser = createUser("John Smith");
9
  assertEquals(actualUser.getId(), mockedUser.getId());
10
  assertEquals(actualUser.getName(), mockedUser.getName());
11
  assertNotNull(actualUser.getIssueDate());
12
}



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:

powermock http

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. 

Java
 




x
22


 
1
@Test
2
public void testCreateUser(){
3
    //Mocking part
4
      User mockedUser = new User(12, "John Smith"); 
5
      new MockServerClient("localhost", 8080)
6
          .when(request()
7
            .withMethod("POST")
8
            .withPath("/server/users")
9
            .withHeader("Content-type", "application/json")
10
            .withHeader("auth", "mySecretKey")
11
            .withBody(exact("{name: '" + mockedUser.getName() + "', id: " + mockedUser.getId() + "}")),
12
            exactly(1))
13
      .respond(
14
      response()
15
      .withStatusCode(201)
16
      .withBody("{ name: 'Joth Smith', id: ' }");
17
    //Asserting part and call to service
18
    User actualUser = createUser("John Smith");
19
    assertEquals(actualUser.getId(), userMockId);
20
    assertEquals(actualUser.getName(), mockedUser.getName());
21
    assertNotNull(actualUser.getIssueDate());
22
}



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:

new test implementationAs 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).
unit test

Opinions expressed by DZone contributors are their own.

Related

  • Effective Engineering Feedback: Software Testing
  • The LLM Selection War Story: Part 2 - The Six LLM Failure Archetypes That Will Wreck Your Production System
  • Agentic Development: My Invisible Dev Team
  • Clean Code in the Age of Copilot: Why Semantics Matter More Than Ever

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook