top of page
HyperTest_edited.png
Connecting Dots
Abstract Lines
07 Min. Read
26 April 2024

How to generate mocks for your test without needing mockito?

Shailendra Singh
📖 Scope of mocking in unit tests

When writing unit tests for code that interacts with external dependencies (like APIs, databases, file systems, or other external services), a developer needs to ensure these dependencies are properly isolated to make tests predictable, fast, and reliable. The key strategies involve mocking, stubbing, and using test doubles (mocks, stubs, and fakes).


Developers achieve this using any of the many mocking frameworks out there (mockito for example), but HyperTest can auto-generate such mocks or stubs without needing any manual intervention or set-up. This solves a lot of problems:


1.Saves time: Developers valuable time is freed up from writing and maintaining mocks and stubs


2. Maintenance of mocks: Hand-written mocks and stubs become stale i.e. the behavior of the systems mocked can change that requires rewriting these artefacts. On the contrary, HyperTest generated mocks are updated automatically keeping them always in sync with the current behaviour of dependencies


3. Incorrect Mocking: When stubbed or mocked using frameworks, developers rely at best on their understanding of how external systems respond. Incorrect stubbing would mean testing against an unreal behavior and leaking errors. HyperTest on the other hand builds stubs and mocks based on real interactions between components. This not only ensures they are created with the right contracts but are also updated when behaviours of dependencies change keeping mocks accurate and up-to-date


So let’s discuss all the different cases where developers would need mocks, how they build them using a framework like mockito and how HyperTest will automate mocking, removing the need to use mockito


1️⃣ Mocking External Services: Downstream calls, 3rd party APIs

Mocking involves creating objects that simulate the behavior of real services. A mock object will return predefined responses to function calls during tests. This is particularly useful for external APIs or any service that returns dynamic data.


Example: Suppose a developer of a service named AccountService in a bank is dependent on the TransactionService to fetch updates on account balances when a user performs credit or debit transaction on his account. The TransactionAPI would look something like this:

curl -X POST 'https://api.yourbank.com/transactions/updateBalance' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer {access_token}' \
-d '{
"customerId": 3,
"transactionAmount": 500
}'

The AccountService has a class AccountService which the developer needs to test without actually needing to call the API. To do so he decides to use mockito and would do the following:

  • Mock Creation: Mock the TransactionAPI  using mock(TransactionAPI.class).


  • Method Stubbing: The updateBalance method of the mock is configured to return a new BalanceUpdateResponse with the specified old and new balances when called with specific arguments.


  • Service Testing: The AccountService is tested to ensure it properly delegates to TransactionAPI and returns the expected result.


  • Assertions and Verifications: After calling performTransaction, assertions are used to check that the returned balances are correct, and verify is used to ensure that TransactionAPI.updateBalance was called with the correct parameters.


This is how his mockito implementation will look like:


import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

public class AccountServiceTest {
    @Test
    public void testPerformTransaction() {
        // Create a mock TransactionAPI
        TransactionAPI mockTransactionAPI = mock(TransactionAPI.class);

        // Setup the mock to return a specific response
        when(mockTransactionAPI.updateBalance(3, 500))
            .thenReturn(new BalanceUpdateResponse(10000, 10500));

        // Create an instance of AccountService with the mocked API
        AccountService accountService = new AccountService(mockTransactionAPI);

        // Perform the transaction
        BalanceUpdateResponse response = accountService.performTransaction(3, 500);

        // Assert the responses
        assertEquals(10000, response.getOldBalance());
        assertEquals(10500, response.getNewBalance());

        // Verify that the mock was called with the correct parameters
        verify(mockTransactionAPI).updateBalance(3, 500);
    }
}

Mocking external services without needing Mockito

HyperTest eliminates all this effort in a jiffy! Service to service interactions called contracts are automatically built by HyperTest by monitoring actual interactions, in this case between the AccountService and TransactionService.

How this happens?


Mocking external services without needing Mockito
  • The HyperTest SDK is set-up on the AccountService and TransactionService

  • It monitors all the incoming and outgoing calls for both AccountService and TransactionService.

  • In this case, the request - response pair i.e. the contract between AccountService - TransactionService is captured by HyperTest. This contract is used as to mock the TransactionService when testing AccountService and vice versa.


Now when the developer wants to test his AccountService class in Accounts Service, HyperTest CLI builds AccountService app locally or at the CI server and calls this request, and supplies the mocked response from TransactionService


HyperTest SDK that tests TransactionService and AccountService separately, automatically asserts 2 things:


  • The TransactionAPI was called with the correct parameters by the AccountService


Mocking
  • Response of the TransactionService, i.e. new balance is same as 10500 or not. If not it reports error like this.


TransactionService
🚨 TAKEAWAY: HyperTest mocks upstream and downstream calls automatically, somethings that 20 lines of mockito code did above. Best thing, it refreshes these mocks as the behavior of the AccountService (requests) or TransactionService (response) change
2️⃣ Database Mocking: Stubbing for Database Interactions

Stubbing is similar to mocking but is typically focused on simulating responses to method calls. This is especially useful for database interactions where you can stub repository or DAO methods.


Example: Now developer of the TransactionService wants to mock the database layer, for which he creates stubs using Mockito. This service retrieves and updates the account balance from the database. The db interface would like this:


public interface AccountRepository {
    int getBalance(int customerId);
    void updateBalance(int customerId, int newBalance);
}

TransactionService


public class TransactionService {
    private AccountRepository accountRepository;

    public TransactionService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    public BalanceUpdateResponse performTransaction(int customerId, int transactionAmount) {
        int oldBalance = accountRepository.getBalance(customerId);
        int newBalance = oldBalance + transactionAmount;
        accountRepository.updateBalance(customerId, newBalance);
        return new BalanceUpdateResponse(oldBalance, newBalance);
    }
}

Unit test with Mockito


  • Mock the database layer AccountRepository

  • Create instance of TransactionService with mocked repository

  • Perform the Transaction

  • Assert the response


import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class TransactionServiceTest {

    @Test
    public void testPerformTransaction() {
        // Mock the AccountRepository
        AccountRepository mockRepository = mock(AccountRepository.class);
        
        // Setup stubs for the repository methods
        when(mockRepository.getBalance(3)).thenReturn(10000);
        doNothing().when(mockRepository).updateBalance(3, 10500);
        
        // Create an instance of TransactionService with the mocked repository
        TransactionService service = new TransactionService(mockRepository);
        
        // Perform the transaction
        BalanceUpdateResponse response = service.performTransaction(3, 500);
        
        // Assert the responses
        assertEquals(10000, response.getOldBalance());
        assertEquals(10500, response.getNewBalance());
        
        // Verify that the repository methods were called correctly
        verify(mockRepository).getBalance(3);
        verify(mockRepository).updateBalance(3, 10500);
    }
}

Mocking the database layer without needing Mockito

HyperTest SDK, that sits on the TransactionService, can mock the database layer automatically without needing to stub db response like explained above with mockito. The SDK performs the same way for database interactions as it did intercepting outbound http (GraphQL / gRPC) call for external services.


In this example, the TransactionService asks the database 2 things:

  1. Query 1: For a given customerId, return the oldBalance or current balance

  2. Query 2: Update the oldBalance to newBalance for the same customerId


HyperTest mocks both these operations for the TransactionService, like shown below:


TransactionService

The outputs are captured as mocks as the SDK looks at the actual query from the traffic. This is then used when TransactionService is tested. This is what HyperTest does:

  • Perform the transaction i.e. replays the request but use the captured output as the mock

  • Compare the response and database query in the RECORD and REPLAY stage and assert newBalance across both the service response and db query


TransactionService
🚨 TAKEAWAY: HyperTest mocks the database layer just like external services, again that 25 lines of mockito code did above. This removes the need to stub db responses

3️⃣ Testing message queues or event streams

Mocking a message queue or event stream is particularly useful in scenarios where you need to test the interaction of your code with messaging systems like RabbitMQ, Kafka, or AWS SQS without actually sending or receiving messages from the real system


Example: Let's say you have a class MessagePublisher that sends messages to a RabbitMQ queue. You can mock this interaction to verify that messages are sent correctly without needing a running RabbitMQ instance.

Java Class to Test:


javaCopy code
public class MessagePublisher {
    private final Channel channel;

    public MessagePublisher(Channel channel) {
        this.channel = channel;
    }

    public void publishMessage(String queueName, String message) throws IOException {
        channel.basicPublish("", queueName, null, message.getBytes());
    }
}

This MessagePublisher class uses an instance of Channel from the RabbitMQ client library to publish messages.

Unit Test with Mockito:


javaCopy code
import com.rabbitmq.client.Channel;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

public class MessagePublisherTest {

    @Test
    public void testPublishMessage() throws IOException {
        // Create a mock Channel
        Channel mockChannel = Mockito.mock(Channel.class);

      // Create an instance of the class under test, passing the mock Channel
        MessagePublisher publisher = new MessagePublisher(mockChannel);

        // Define the queue name and the message to be sent
        String queueName = "testQueue";
        String message = "Hello, world!";

      // Execute the publishMessage method, which we are testing
        assertDoesNotThrow(() -> publisher.publishMessage(queueName, message));

        // Verify that the channel's basicPublish method was called with the correct parameters
        Mockito.verify(mockChannel).basicPublish("", queueName, null, message.getBytes());
    }
}

Explanation:

  • Mock Creation: The Channel class is mocked. This is the class provided by the RabbitMQ client library for interacting with the queue.


  • Method Testing: The publishMessage method of the MessagePublisher class is tested to ensure it calls the basicPublish method on the Channel object with the correct parameters.


  • Verification: The test verifies that basicPublish was called exactly once with the specified arguments. This ensures that the message is formatted and sent as expected.


Mocking Queue Producers & Consumers without needing Mockito

HyperTest use the same technique to capture interactions between a queue producer and consumer. This gives it the ability to verify if producer is sending the right messages to the broker, and if the consumer is doing the right operations after receiving those messages.


When testing producers, HyperTest will assert for:

  • Schema: The data structure of the message i.e. string, number etc

  • Data: The exact values of the message parameters


Here is the example of how HyperTest has mocked the broker for the producer by capturing the outbound calls (expected downstream call) and asserting the outbound messages by comparing the actual message (real downstream call) with one captured message.


kafka

The same way the HyperTest SDK on the respective consumer would test all side effects by asserting consumer operations


🚨 TAKEAWAY: HyperTest mocks the broker for both the queue producer and consumer in an actual end to end event flow. It can then test the producer and consumer separately. This automates mocking and testing an async event flow without the need of mocking frameworks like mockito or others.  

Connecting Dots

Prevent Logical bugs in your databases calls, queues and external APIs or services

bottom of page