Mocks, Spies, and Stubs: How to Use?
Mocks, Spies, and Stubs are three different types of test doubles that we use in software testing. These are mainly used in unit testing, API testing, etc. We use mocks, stubs, and spies to simulate the behavior of complex, real objects. These tools are essential for isolating the component under test and ensuring that the tests are not affected by external dependencies. Let’s discuss each of these more and how we use them in testing.
Mocks
When testing software, you may need a way to imitate complex parts of your system to check the smaller, more isolated pieces. Let’s consider rehearsing a play. Sometimes, not all actors are available, so you might have stand-ins to help practice scenes. In software testing, mocks are these stand-ins, but for software components.
Mocks are fake versions of real software components that programmers use during testing. They are designed to mimic the behavior of real objects in controlled ways. By using mocks, you can ensure that the part of the software they are testing works correctly without needing the actual software components, which might be unreliable, slow, or difficult to set up for testing.
Why Use Mocks?
Imagine you are developing an app that needs to fetch weather data from an online service. To test the app’s response to different weather conditions, you wouldn’t want to rely on real-time weather data because it’s unpredictable and beyond your control. Instead, you could use a mock to simulate the weather service. You can program the mock to return predefined weather data (like “sunny” or “rainy”) so you can see how your app behaves under those conditions without connecting to the actual weather service.
This approach has several benefits:
- Isolation: Mocks help isolate the part of the system you want to test from its dependencies. For instance, if a function needs to fetch data from a database, you don’t want your tests to fail just because the database is unavailable or slow. By mocking the database, you ensure the test only focuses on the logic of the function itself.
- Control: With mocks, you have control over how dependent services behave. You can make a mocked database throw an error on demand or make a mocked service return specific data. This control is crucial for testing how your code responds to different scenarios.
- Efficiency: Using mocks makes your tests run faster because you avoid performing time-consuming tasks like real database queries or network calls.
- Simplicity: Tests with mocks can be simpler to write and understand because they involve setting up expected outcomes and checking whether those outcomes occur.
How Do Mocks Work?
Mocks work by replacing real objects in your system with fake ones that you create just for your tests. These fake objects are programmed to behave in specific ways. Let’s consider an example: For a function in your code, it needs to call another service, and that service returns the value as either “Yes” or “No”. So, to check your function, you can create a mock of that service which always returns the value “Yes”. With this, you can test your function.
Here’s a simple step-by-step explanation of using a mock:
- Identify the dependency: This is the component or service your code depends on, but you want to replace it with a mock for testing.
- Create the mock: Use a mocking tool or framework to create a mock object that mimics the dependency.
- Program the behavior: Set up the mock to return specific data or behave in a certain way when your code interacts with it.
- Run the test: Execute your test code using the mock instead of the real dependency.
- Verify the results: Check if your code behaved as expected when it used the mock.
When We Use Mocks?
There are several situations in which we can use a mock.
- Non-Deterministic Behavior: When testing functions or methods that yield unpredictable results, such as generating random values or retrieving current date and time.
- Complex Setups: If setting up a real object involves complex initialization that is impractical for repeated testing, mocks can simplify the process by mimicking the necessary parts of the object.
- Difficult-to-Trigger Conditions: Mocks are useful when you need to test how your application handles rare or extreme conditions, such as disk full errors, network failures, or resource exhaustion.
- Slow Operations: When real operations are slow, like sending emails, querying databases, or communicating over networks, mocks allow you to simulate these operations quickly and reliably.
- User Interfaces: Mocking user interfaces can help test the underlying business logic without the need for complex UI interactions, which are cumbersome to automate.
- Callback Functions: These are used to test asynchronous callbacks without having to deal with actual asynchronous behavior, which can simplify testing and make it more predictable.
- Components Not Yet Implemented: When certain parts of the application are under development and not available, mocks can simulate these components to allow testing of other parts of the system.
- Legacy Code Integration: In legacy systems where making changes is risky or difficult, mocks can help encapsulate and test the old code effectively.
Examples of Using Mocks
from unittest.mock import Mock # Suppose we have a function that checks stock availability def check_stock(product_id, database): return database.get_stock(product_id) > 0 # Create a mock database mock_database = Mock() # Configure the mock to return a specific value mock_database.get_stock.return_value = 10 # Use the mock in our function product_is_in_stock = check_stock("1234", mock_database) # Verify assert product_is_in_stock == True mock_database.get_stock.assert_called_with("1234")
In this example, mock_database is a mock version of a database. We configure it to always return ’10’ when get_stock is called, regardless of the input. We then check that check_stock behaves correctly when get_stock returns a positive number, and we also verify that the right database method was called with the right arguments.
Best Practices
When using mocks, it’s important to follow some best practices:
- Don’t Overuse Mocks: While mocks are useful, overusing them can lead to tests that are brittle and don’t actually prove that your program works as a whole. Use them when necessary to isolate components or when interfacing with external systems.
- Keep It Realistic: Always configure mocks to mimic the realistic behavior of the objects they’re standing in for. Unrealistic mock configurations can lead to tests that pass during testing but fail in production.
- Clean Up: Make sure that each test is independent; reset or recreate mocks between tests if needed.
- Documentation: Keep clear documentation of what each mock is supposed to be doing. This makes tests easier to understand and maintain.
Spies
Spies are special tools that let you watch how parts of your program behave during tests without stopping them from doing their normal work. They’re like secret agents in your code, watching and recording information silently. Understanding spies can help you ensure your tests are thorough and that your program behaves exactly as expected under various conditions.
A spy is a type of test double, which we can say is a fake version of a function or a method that you use in tests. However, unlike mocks or stubs that replace the whole function with fake behavior, spies wrap the real function, allowing it to run normally while secretly recording information about how it was used. For example, spies can track how many times a function was called or what arguments were passed to it.
Why Use Spies?
Spies are incredibly useful because they give you insight into how your code operates without changing its behavior. Here are some of the main reasons why developers use spies in their tests:
- Transparency: Spies provide a clear view of how functions and methods are called during the test without modifying what these functions actually do.
- Verification: With spies, you can verify that your code is calling certain functions in the right way. For instance, you can check whether a function was called with specific arguments, how many times it was called, and in what order functions were called.
- Non-Intrusive: Since spies allow functions to operate as usual, they are non-intrusive. This means you can test the natural behavior of your application under real conditions.
- Flexibility: Spies can be used in combination with other testing tools, like mocks and stubs, providing a flexible way to build comprehensive tests.
How Do Spies Work?
Implementing spies typically involves the following steps:
- Setup: First, you attach a spy to the function or method you want to observe. This is done using a spying tool or library specific to the programming language you are working with.
- Execution: You then run your test, during which the spied-on function executes normally. The spy silently records all the relevant interactions.
- Inspection: After the test, you can examine the information collected by the spy to understand how the function was called, what data was passed to it, what it returned, and other details.
When We Use Spies?
Here are several key scenarios where we use Spies:
- Function Call Verification: To ensure that certain functions are called as expected. Spies can verify that functions are called with the correct arguments, the right number of times, and in the correct order.
- Event Handling: When testing event-driven programming, spies can confirm that event handlers are triggered appropriately and handle the events as intended.
- Testing Callbacks: In asynchronous programming, spies can be used to check whether callback functions are invoked correctly, helping to test complex flows of asynchronous operations.
- Method Behavior Tracking: Spies are perfect for observing how methods interact within a class or between components without altering their actual implementation, providing insights into the dynamics of your application.
- Conditional Logic Testing: They help in verifying that conditional logic branches in your code are executed under various scenarios, ensuring all parts of your application behave as expected under different conditions.
- Integration Points: Spies can be used to monitor interactions at integration points within an application to ensure that components integrate smoothly without needing to invoke actual integrations during tests.
- Performance Checks: While not directly related to performance testing, spies can help identify potentially costly methods by tracking how often they are called and under what circumstances.
- Dependency Interactions: To observe and ensure that objects interact correctly with their dependencies, which is crucial when you want to ensure that your mocks or stubs are integrated correctly within the test environment.
Examples of Using Spies
// Function to test function addToCart(item) { cart.push(item); updateTotal(); // We'll spy on this function } // Test test('updateTotal is called when an item is added to the cart', () => { const spy = jest.spyOn(cart, 'updateTotal'); addToCart({ item: 'Book', price: 9.99 }); expect(spy).toHaveBeenCalled(); // Check if the spy recorded a call to updateTotal spy.mockRestore(); // Clean up the spy });
In this test, the spy helps confirm that not only does the addToCart function add the item, but it also correctly calls the updateTotal method to update the cart’s total, verifying the interaction without affecting the operation.
Best Practices for Using Spies
- Use Judiciously: While spies can be incredibly helpful, they should be used carefully. Overusing spies can make tests difficult to manage and understand.
- Combine with Other Tools: Spies are most effective when used with other types of test doubles like mocks and stubs, depending on the needs of your test scenarios.
- Clean Up: Always ensure that spies are removed or restored after each test to prevent them from affecting other tests.
- Documentation: Document the use of spies in your tests well. This helps maintain clarity and understandability in your test suites, making it easier for others to understand what each test aims to achieve.
Stubs
Stubs are simplified, controlled replacements for complex or unwieldy components your code interacts with. Like actors on a movie set who stand in for stars during the complicated setup of scenes, stubs stand in for real components in your tests, making the testing process smoother and more focused.
A stub is a type of test double – a term used in software testing to describe any object or component that replaces a real component purely for testing purposes. Stubs are programmed to return specific responses to calls made during a test. They don’t attempt to replicate the complete behavior of the component they replace; instead, they provide predetermined responses to specific input.
For example, if your application needs to fetch weather data from an external service, you might use a stub to simulate this service. The stub would return fixed weather data when queried, so you can test how your application behaves with that data without needing to rely on the external service during tests.
Why Use Stubs?
Stubs are particularly useful because they help isolate the part of the system you are testing. This isolation helps you verify that your application behaves correctly with known inputs and can handle various outputs the real component might produce. Here are some key reasons to use stubs:
- Controlled Environment: Stubs allow you to create a controlled testing environment where you can specify exactly what data the component under test receives. This makes your tests predictable and repeatable.
- Simplicity and Efficiency: They simplify testing by eliminating dependencies on external systems or components that are complex, slow, or unreliable. This not only speeds up testing but also makes it less costly.
- Focus on Integration: When testing how different parts of your application interact, stubs can simulate the parts outside of the focus area, allowing you to concentrate on the integration logic.
- Handling Edge Cases: Stubs can be programmed to return exceptional data or error conditions. Hence, you can test how your application handles these situations without the need to configure complex scenarios.
How Do Stubs Work?
Using a stub typically involves the following steps:
- Identify the Component to Replace: Determine which component or function in your application should be replaced by a stub during testing. This component usually interacts with external systems or services.
- Create the Stub: Implement a stub that can replace the identified component. This stub should be able to accept the same inputs as the component it replaces and provide appropriate outputs.
- Program the Stub: Define what outputs the stub should return in response to specific inputs. These outputs can be normal data, error messages, or any other responses that the real component might provide.
- Integrate the Stub: In your test environment, replace the real component with the stub. This might involve configuring your application to use the stub instead of the real component.
- Run Tests: Execute your tests with the stub in place. Since the stub returns predictable responses, you can test how your application reacts to those responses.
When We Use Stub?
There are several key situations where using stubs is particularly beneficial:
- External Service Simulation: When your application depends on external services (like web APIs, databases, or third-party services), stubs can simulate these services to ensure your tests are not affected by their availability or variability.
- Complex System Interaction: For components that interact with complex systems that are difficult or impractical to include in every test run, stubs can mimic the necessary parts of those systems to facilitate testing.
- Non-Deterministic Outputs: In cases where a component’s output is non-deterministic (such as timestamps, random values, or sensor readings), stubs can provide fixed, predictable outputs for consistent testing.
- Long-Running Processes: If testing involves components that are slow or resource-intensive, such as long computations or data processing tasks, stubs can replace these with immediate responses to speed up test execution.
- Limited Access Components: When you have limited access to certain components due to security, cost, or logistical reasons (like SMS gateways or email servers), stubs can stand in to simulate these components.
- Testing Error Handling: Stubs can simulate various failure modes and error conditions to test how the application handles such situations, ensuring robust error handling and fault tolerance.
- Unavailable Components: During the development process, some components may not yet be implemented. Stubs can be used to mimic these components’ interfaces, allowing developers to continue testing and developing other parts of the system.
- Simplifying Complex Dependencies: In a system with complex dependencies, stubs can simplify the dependencies for the purpose of testing a particular component, making tests easier to write and understand.
Example of Using a Stub
class TaxServiceStub: def get_tax_rate(self): # Always return a fixed tax rate return 0.05 # In your tests def test_calculate_total_with_tax(): cart = ShoppingCart(items=[Item(price=100)]) tax_service = TaxServiceStub() total = cart.calculate_total_with_tax(tax_service) assert total == 105 # 100 + 5% tax
In this example, TaxServiceStub is a stub that simulates the tax calculation service by always returning a fixed tax rate of 5%. This allows the test to focus on the correctness of the calculate_total_with_tax method in the ShoppingCart class.
Best Practices for Using Stubs
- Use Stubs for External Dependencies: They are best used for components that interact with external systems (e.g., web services, databases) or components that have non-deterministic behavior.
- Keep Stubs Simple: Avoid making stubs too smart. They should be simple enough to provide specific responses without incorporating logic that mimics the real component too closely.
- Ensure Test Coverage: While stubs help isolate tests, ensure that the integration with the real components is also tested somewhere in your test suite, typically in higher-level integration or end-to-end tests.
- Document the Behavior: Clearly document the behavior of your stubs so that anyone running the tests understands what to expect. This is crucial for maintaining and extending tests.
Mocks vs. Spies vs. Stubs
Feature | Mocks | Spies | Stubs |
Purpose | Primarily used to verify interactions. | Used to gather information about how a function is called. | Used to provide predetermined responses to calls. |
Usage | This is to ensure that certain methods are called with the expected arguments. | To observe method calls and record information without affecting their behavior. | To replace complex or unavailable components with simplified ones that return fixed data. |
Functionality | Can enforce that expected actions are taken within the code. | Do not alter the behavior of the function; just track it. | Do not track calls or interactions; only return specific responses. |
Control | High control: Mocks can dictate the behavior of the test scenario. | Medium control: Spies allow the original method to execute and record the calls. | Low control: Stubs only respond to calls with predefined outputs. |
Interaction | It can be used to assert on the method calls and interactions. | Used to verify that methods were called correctly, but not to enforce it. | No verification of method calls; purely a response provider. |
Complexity | High: Requires setup to define expected interactions. | Medium: Requires setup only to record interactions. | Low: Simply return fixed values or states. |
Typical Use | To test the interactions between objects in isolation from their dependencies. | To monitor existing functionality during a test to ensure correct behavior. | To simulate external services or components to ensure the tested code runs in isolation. |
Tools for Creating Mocks, Spies, and Stubs
Different programming languages and environments have their own tools designed specifically for this purpose. Let’s list a few popular ones in each language.
- Java: Mockito, JUnit, EasyMock
- JavaScript: Jest, Sinon.js, Mockery
- Python: unittest.mock, Mockito-Python, Pytest-mock
- Ruby: RSpec, Mocha
- C++: Google Mock, Turtle
Mocking API calls with testRigor
mock api call get "https://dummy.restapiexample.com/api/v1/employees" with headers "a:a" returning body "This is a mock response" with http status code 200
In the example above, any GET calls to the endpoint “https://dummy.restapiexample.com/api/v1/employees” with the headers “a:a” will respond with the testRigor mock, with status ‘200’.
Some useful cases are:
- You might want to use mocking APIs if you are using third-party API calls. Those calls can be charged and expensive, so you can mock the responses instead of calling the real service.
- You can test your application individually if servers are unstable or down.
- You can test specific scenarios, for example, if you want to test a scenario where the server returns an error.
To know how we can do API testing using testRigor, you can read this blog – How to do API testing using testrigor. Apart from that, testRigor supports automating complex scenarios such as 2-factor authentication, QR codes, CAPTCHAs, email testing, SMS testing, table data, and more with simple English commands. Find the top testRigor’s features here.
Conclusion
Mocks, spies, and stubs are special tools that help us make sure every part of our software works correctly on its own without any problems. Without these tools, it would be tough to check each component separately. By using mocks, spies, and stubs in our tests, we can make our applications more stable and reliable. These tools help developers create clearer and more specific tests, giving us the confidence that our software will work well in different situations. Therefore, it’s always a best practice to use these test doubles to ensure a high-quality application.
Frequently Asked Questions (FAQs)
Choosing between Mock, Stub, and Spy depends on the function or method that you need to test. If you need to assert behavior, then you can use mocks. If you need to observe the behavior without altering it, you can go with spies. If you need to isolate the method from external dependencies by providing an expected response, then you can use stubs.
Yes, we can combine these test doubles based on the scenario we are testing. For example, if the scenario is very complex and requires you to isolate, observe, and verify different aspects of the system, then you can use them combined.
Achieve More Than 90% Test Automation | |
Step by Step Walkthroughs and Help | |
14 Day Free Trial, Cancel Anytime |