Mocking and Stubbing in Services Testing
Tags: beginners, testing, programming, qa
Published: 2023-04-30 11:03:23

Testing is an essential part of software development. As software becomes more complex and interconnected, testing becomes more important to ensure that the system behaves as expected. One popular approach to testing backend services is mocking and stubbing. While this approach has many advantages, it also has some limitations. In this article, we will explore the advantages and limitations of mocking and stubbing in backend services testing, along with some best practices for using this approach effectively.

## What is Mocking and Stubbing?
Mocking and stubbing are two techniques used to replace real objects with fake ones during testing. In backend services testing, we use these techniques to replace external dependencies, such as databases or APIs, with mock objects or stubs. A mock object is a dummy object that mimics the behaviour of a real object. A stub, on the other hand, is a pre-programmed object that returns a specific value or set of values.

## Advantages
* Speed up test execution: Mocking and stubbing can speed up test execution by reducing the number of external dependencies that need to be invoked. By replacing these dependencies with mock objects or stubs, we can run tests faster and more frequently.
* Reduce dependencies: External dependencies can be a source of complexity and instability. By replacing these dependencies with mock objects or stubs, we can reduce the number of dependencies and simplify the testing process.
* Increase control over test cases: Mocking and stubbing allow us to control the behaviour of external dependencies and focus on specific scenarios. This enables us to create more targeted and focused test cases, which can improve the effectiveness of our testing.
* Enable testing of error cases: Mocking and stubbing allow us to simulate error conditions that may be difficult or impossible to reproduce in real-world scenarios. This enables us to test the resilience and fault-tolerance of our backend services in a controlled environment.

## Limitations
* Limited scope of testing: Mocking and stubbing are limited in scope to the specific functionality being tested. This means that they cannot test the interaction between different components or services, which can lead to false positives and incomplete testing.
* Possibility of false positives: Mocking and stubbing can create false positives, where tests pass even though the system is not functioning correctly. This can happen if the mock objects or stubs do not accurately simulate the behaviour of the real objects.
* Complexity of creating and maintaining mocks/stubs: Creating and maintaining mock objects and stubs can be complex and time-consuming. This can make it difficult to maintain test code and increase the risk of errors and bugs.
* Inability to test integration with external services: Mocking and stubbing cannot test the integration between different services or systems. This means that they cannot fully test the end-to-end behaviour of the system.

## Best Practices
While mocking and stubbing have many advantages, they should be used sparingly and thoughtfully. Here are some best practices for using mocking and stubbing effectively:

* Use sparingly and thoughtfully: Use mocking and stubbing only when necessary, and focus on testing the most critical and complex parts of the system.
* Avoid overcomplicating the test code: Mocking and stubbing can add complexity to the test code. Avoid overcomplicating the code by using simple and clear tests that focus on the essential functionality.
* Use real-world data to make the tests more realistic: Use real-world data to make the tests more realistic and improve their effectiveness. This can help ensure that the tests are more closely aligned with real-world scenarios and catch edge cases that may be missed with synthetic data.
* Always validate assumptions with integration tests: Mocking and stubbing should be used in conjunction with integration tests to ensure that the system behaves correctly end-to-end. Use integration tests to validate assumptions and catch any issues that may have been missed with mocking and stubbing.

## Real-world Examples
To illustrate the advantages and limitations of mocking and stubbing in backend services testing, let's consider some real-world examples.

### Example of successful use
A team is developing a payment processing service that integrates with a third-party payment gateway. To test the service, they use mocking and stubbing to simulate the behavior of the payment gateway. By doing so, they can test the various error conditions that may occur during payment processing, such as declined transactions or network timeouts. This approach allows them to identify and fix issues before deploying the service to production.

### Example of misuse
A team is developing an e-commerce platform that integrates with a product catalog API. To test the platform, they use mocking and stubbing to replace the product catalog API with a mock object. However, they do not thoroughly test the integration between the platform and the product catalog API, which leads to issues in production. In this case, mocking and stubbing were misused and did not provide adequate testing coverage.

## Code Examples
{% spoiler Mocking Example %}
In this example, we have a class called `UserService` which depends on a class called `Database`. The `Database` class is responsible for storing user data. We want to test the `UserService` class, but we don't want to actually interact with a real database during testing. Instead, we can use a mock object to simulate the behaviour of the `Database` class.

```python
class UserService:
    def __init__(self, db):
        self.db = db

    def get_user(self, user_id):
        user = self.db.query('SELECT * FROM users WHERE id = ?', user_id)
        return user

# Mock object for the Database class
class MockDatabase:
    def query(self, sql, *args):
        return {'id': 1, 'name': 'John Doe'}

# Testing the UserService class with a mock object
def test_get_user():
    mock_db = MockDatabase()
    user_service = UserService(mock_db)
    user = user_service.get_user(1)
    assert user == {'id': 1, 'name': 'John Doe'}
```

In this example, we create a `MockDatabase` class which has a `query` method that returns a hard-coded user object. We then create an instance of the `UserService` class, passing in the `MockDatabase` instance. Finally, we call the `get_user` method on the `UserService` instance and assert that the returned user object matches our expected result.
{% endspoiler %}

{% spoiler Stubbing Example %}
In this example, we have a class called `ProductService` which depends on a class called `ProductRepository`. The `ProductRepository` class is responsible for fetching product data from a remote API. We want to test the `ProductService` class, but we don't want to actually call the remote API during testing. Instead, we can use a stub object to replace the `ProductRepository` class with a simplified implementation that returns hard-coded product data.

```python
class ProductService:
    def __init__(self, repo):
        self.repo = repo

    def get_products(self):
        products = self.repo.fetch_products()
        return products

# Stub object for the ProductRepository class
class StubRepository:
    def fetch_products(self):
        return [
            {'id': 1, 'name': 'Product 1'},
            {'id': 2, 'name': 'Product 2'},
            {'id': 3, 'name': 'Product 3'}
        ]

# Testing the ProductService class with a stub object
def test_get_products():
    stub_repo = StubRepository()
    product_service = ProductService(stub_repo)
    products = product_service.get_products()
    assert len(products) == 3
    assert products[0]['name'] == 'Product 1'
```

In this example, we create a `StubRepository` class which has a `fetch_products` method that returns hard-coded product data. We then create an instance of the `ProductService` class, passing in the `StubRepository` instance. Finally, we call the `get_products` method on the ProductService instance and assert that the returned products match our expected results.
{% endspoiler %}

## Conclusion
Mocking and stubbing can be valuable techniques for testing backend services, but they also have limitations. It's important to use mocking and stubbing sparingly and thoughtfully, and to always validate assumptions with integration tests. By following best practices and understanding the advantages and limitations of this approach, we can use mocking and stubbing effectively to improve the quality and reliability of our software.