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

Simplify Your Code: A Guide to Mocking for Developers

Shailendra Singh
Vaishali Rastogi

You want to test your code but avoid dependencies?


The answer is “Mocking”.


Mocking comes handy whenever you want to test something that has a dependency. Let’s talk about mocking first in a little detail.



What’s mocking, anyway?


The internet is loaded with questions on mocking, asking for frameworks, workarounds and a lot more “how-to-mock” questions. But in reality, when discussing testing, many are unfamiliar with the purpose of mocking.


how to mock

Let me try by giving an example:


💡 Consider a scenario where you have a function that calculates taxes based on a person's salary, and details like salary and tax rates are fetched from a database. Testing with a database can make the tests flaky because of database unavailability, connection issues, or changes in contents affecting test outcomes. 

Therefore, a dev would just simply mock the database response i.e. the income and tax rates for the dummy data he is running his unit tests on. By mocking database interactions, results are deterministic which is what devs desire.

Hope the concept is clear now, but when everything seems good with mocking, what’s the purpose of this article? Continue reading to get the answer to this question.



All seems good with mocking, what’s the problem then?


API mocking is typically used during development and testing as it allows you to build your app without worrying about 3rd party APIs or sandboxes breaking.



Harlan haskins tweet

Amada tweet

But evidently, people still got some issues with mocking!


Mocking Too Much is still a hot topic of discussion among tech-peers, but why do they have this opinion in the first place? This article is all about bringing out the real concerns people have with mocking. And presenting you a way that takes away all the mocking-related pain.



1️⃣ State Management Complexity

Applications flows are fundamentally stateless. But database imputes state in a flow because it makes a flow contextual to a user journey. Imagine testing checkout, to do so the application should be in a state where a valid user has added a valid SKU with the required inventory. This means before running a test, we need to fill the database with the required data, execute the test, and then clean out the database once the test is over. This process, however, repetitive, time-consuming and with diminishing returns.



Now, consider the complexity of handling numerous user scenarios. We'd have to prepare and load hundreds, maybe thousands, of different user data setups into the database for each test scenario.



2️⃣ False Positives/Negatives

False positives in tests occur when a test incorrectly passes, suggesting code correctness despite existing flaws. This often results from mocks that don't accurately mimic real dependencies, leading to misplaced confidence.


Conversely, false negatives happen when tests fail, indicating a problem where none exists, typically caused by overly strict or incorrect mock setups.


Both undermine test reliability—false positives mask bugs, while false negatives waste time on non-issues. Addressing these involves accurate mock behavior, minimal mocking, and supplementing with integration tests to ensure tests reflect true system behavior and promote software stability.



3️⃣ Maintenance Overhead

Assume UserRepository is updated to throw a UserNotFound exception instead of returning None when a user is not found. You have to update all tests using the mock to reflect this new behavior.


# New behavior in UserRepository
def find_by_id(user_id):
    # Throws UserNotFound if the user does not exist
    raise UserNotFound("User not found")

# Updating the mock to reflect the new behavior
mock_repository.find_by_id.side_effect = UserNotFound("User not found")

Keeping mocks aligned with their real implementations requires continuous maintenance, especially as the system grows and evolves.



HyperTest’s way of solving these problems


We have this guide on why and how on HyperTest, just go through it once and then

hop over here.


To give you a brief:

💡 HyperTest makes integration testing easy for developers. What’s special is its ability to mock all the third-party dependencies including your databases, message queues, sockets and of course the dependent services. 

This behavior of autogenerating mocks that simulate dependencies not only streamline the test creation but also allow you to meet your development goals faster.


The newer approach towards mocking


Let’s understand this HyperTest approach by quoting an example scenario to make things easy to understand and explain.


So imagine we’ve a shopping app and we need to write integration tests for testing it.


💡 The Scenario

Imagine we have a ShoppingCartService class that relies on a ProductInventory service to check if products are available before adding them to the cart. The ProductInventory service has a state that changes over time; for example, a product might be available at one moment and out of stock the next.


class ShoppingCartService:
    def __init__(self, inventory_service):
        self.inventory_service = inventory_service
        self.cart = {}

    def add_to_cart(self, product_id, quantity):
        if self.inventory_service.check_availability(product_id, quantity):
            if product_id in self.cart:
                self.cart[product_id] += quantity
            else:
                self.cart[product_id] = quantity
            return True
        return False

💡The Challenge

To test ShoppingCartService's add_to_cart method, we need to mock ProductInventory's check_availability method. However, the availability of products can change, which means our mock must dynamically adjust its behavior based on the test scenario.


💡Implementing Stateful Behavior in Mocks

To accurately test these scenarios, our mock needs to manage state. HyperTest’s ability to intelligently generate and refresh mocks gives it the capability to test the application exactly in the state it needs to be.


To illustrate this, let's consider the shopping scenario again. Three possible scenarios can occur:


  1. The product is available, and adding it to the cart is successful.

  2. The product is not available, preventing it from being added to the cart.

  3. The product becomes unavailable after being available earlier, simulating a change in inventory state.


HyperTest SDK will record all of these flows from the traffic, i.e., when the product is available, when the product is not available and also when there’s a change in the inventory state.


In its test mode, when HyperTest runs all the three scenarios, it will have the recorded response from the database for all, testing them in the right state to report a regression if either of the behaviors regresses.


I’ll now delve into how taking advantage of HyperTest’s capability of auto-generating mocks one can pace up the work and eliminate all the mocking-problems we discussed earlier.


1. Isolation of Services for Testing

Isolating services for testing ensures that the functionality of each service can be verified independently of others. This is crucial in identifying the source of any issues without the noise of unrelated service interactions.


HyperTest's Role: By mocking out third-party dependencies, HyperTest allows each service to be tested in isolation, even in complex environments where services are highly interdependent. This means tests can focus on the functionality of the service itself rather than dealing with the unpredictability of external dependencies.


HyperTest's Role

2. Stability in Test Environments

Stability in test environments is essential for consistent and reliable testing outcomes. Fluctuations in external services (like downtime or rate limiting) can lead to inconsistent test results.


HyperTest's Role: Mocking external dependencies with HyperTest removes the variability associated with real third-party services, ensuring a stable and controlled test environment. This stability is particularly important for continuous integration and deployment pipelines, where tests need to run reliably at any time.


3. Speed and Efficiency in Testing

Speed and efficiency are key in modern software development practices to enable rapid iterations and deployments.


HyperTest's Role: By eliminating the need to interact with actual third-party services, which can be slow or rate-limited, HyperTest significantly speeds up the testing process. Tests can run as quickly as the local environment allows, without being throttled by external factors.


4. Focused Testing and Simplification

Focusing on the functionality being tested simplifies the testing process, making it easier to understand and manage.


HyperTest's Role: Mocking out dependencies allows testers to focus on the specific behaviors and outputs of the service under test, without being distracted by the complexities of interacting with real external systems. This focused approach simplifies test case creation and analysis.



Let’s conclude for now


HyperTest's capability to mock all third-party dependencies provides a streamlined, stable, and efficient approach to testing highly inter-dependent services within a microservices architecture.


This capability facilitates focused, isolated testing of each service, free from the unpredictability and inefficiencies of dealing with external dependencies, thus enhancing the overall quality and reliability of microservices applications.

Connecting Dots

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

bottom of page