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?
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
Response of the TransactionService, i.e. new balance is same as 10500 or not. If not it reports error like this.
🚨 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:
Query 1: For a given customerId, return the oldBalance or current balance
Query 2: Update the oldBalance to newBalance for the same customerId
HyperTest mocks both these operations for the TransactionService, like shown below:
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
🚨 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.
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.