Purpose
When developing, you may encounter the following concerns:
- Why are my tests breaking like this? Is this correct?
- Mocking this is so hard…!
- Why are there so many predefined states? How do I manage them?
A good example to explain this is understanding the difference between stubs and mocks. This post explains that difference.
Why? It helps explain integration and unit testing more clearly.
Source: https://martinfowler.com/articles/mocksArentStubs.html
Key Terms
Test Double
A general term for objects that pretend to be real objects for testing purposes. Types of test doubles include Dummy, Fake, Spies, Stubs, and Mocks. The term comes from “stunt double” in movies—think of someone performing dangerous actions in place of the actor.
Unit Test Example
Target Class
Order
- Order form
- Fills the order from the Warehouse
|
|
Warehouse
- Warehouse
- Manages products and quantities
|
|
Target Logic
Logic for Filling Orders from Warehouse
- If there is enough stock in the warehouse → fill the order and deduct stock from the warehouse.
- If not enough stock → do not fill the order, nothing happens.
Target Method
- order.fill(Warehouse)
Collaborators
For unit testing, you want to test only the order object. However, order.fill
requires a collaborator: Warehouse. That is, order.fill
calls methods on Warehouse.
So:
- What kind of test double should you use for warehouse?
- How do you verify that warehouse’s methods were called by order?
Test Code
STUB Style
|
|
Explanation
- Use a real warehouse object and initialize its state.
- After the exercise, check if the order is in the expected state to verify the method worked.
- After the exercise, check if the warehouse is in the expected state to verify the method worked.
In other words, you verify by checking the final state after initializing the warehouse, so this is called state verification.
MOCK Style
|
|
Explanation
- Use a mock object instead of a real warehouse.
- During setup, add expectation settings for the warehouse mock.
- Add verification that the method was called on the warehouse.
By verifying whether the method was called, you check if the method worked, so this is called behavior verification.
In OOP, a method is often called a behavior - wiki
What if the collaborator’s behavior changes?
as-is
|
|
to-be
|
|
|
|
If the collaborator’s method signature changes, what happens to the test code?
stub
The fill method still works correctly.
mock
Mocks directly depend on the collaborator’s method, so the test breaks.
Therefore, you must update the expectation for the collaborator’s method for the test to work.
This is where, even in unit tests, you need to know the implementation details of the collaborator.
Test - Summary
- stub (state verification)
- Uses real objects
- Verifies by checking the final state of the collaborator
- Initializes the collaborator’s state
- Less likely to break if the collaborator’s method changes
- mock (behavior verification)
- Uses mock objects
- Verifies by checking if the collaborator’s method was called
- Initializes the expected behavior of the collaborator
- More likely to break if the method changes
Pros and Cons of State vs Behavior Verification
Generalizing stub vs mock as state vs behavior verification:
State Verification (classicist or Detroit school) | Behavior Verification (mockist or London school) | |
---|---|---|
Fixture setup | • More fixtures to initialize • More states to initialize • Need management policy if reusing state | • Fewer fixtures to initialize |
Test isolation | • Lower isolation • Using real objects, if a bug occurs, many tests depending on the collaborator may break | • Higher isolation • Only tests with incorrect expectations break |
Coupling with implementation | Lower coupling • The test target does not directly reference the collaborator, only verifies state | Higher coupling • The test target directly references the collaborator’s method |
Execution speed | Can be slower • Uses real objects | Faster |
- Regression: Bugs that occur after code changes
- Regression prevention: How well tests prevent bugs after changes
- Integration tests using real objects have higher regression prevention
- Refactoring resilience: If tests break even when functionality is unchanged, resilience is low
- Indicates how likely tests are to break when code structure is changed without changing results