Double lets you mock trait implementations so that you can track function call arguments and set return values or overrides functions at test time.
Here's a quick example:
#[macro_use]
extern crate double;
// Code under test
trait BalanceSheet {
fn profit(&self, revenue: u32, costs: u32) -> i32;
}
fn double_profit(revenue: u32, costs: u32, balance_sheet: &BalanceSheet) -> i32 {
balance_sheet.profit(revenue, costs) * 2
}
// Test which uses a mock BalanceSheet
mock_trait!(
MockBalanceSheet,
profit(u32, u32) -> i32);
impl BalanceSheet for MockBalanceSheet {
mock_method!(profit(&self, revenue: u32, costs: u32) -> i32);
}
fn test_doubling_a_sheets_profit() {
// GIVEN:
let sheet = MockBalanceSheet::default();
sheet.profit.return_value(250);
// WHEN:
let profit = double_profit(500, 250, &sheet);
// THEN:
// mock returned 250, which was doubled
assert_eq!(500, profit);
// assert that the revenue and costs were correctly passed to the mock
sheet.profit.has_calls_exactly_in_order(vec!((500, 250)));
}
// Executing test
fn main() {
test_doubling_a_sheets_profit();
}
More examples are available in the examples directory.
Mocking a trait requires two steps. One to generate the mock struct that will implement the mock and another to generate the bodies of the mocked trait methods.
For step one, we use the mock_trait macro. This takes the name of the mock struct to generate and a list specifying all of the trait's methods, their arguments (omitting self) and their return values (specifying -> () if the method does not return a value).
Consider the example below:
trait BalanceSheet {
fn profit(&self, revenue: u32, costs: u32) -> i32;
fn clear(&mut self);
}
mock_trait!(
MockBalanceSheet,
profit(u32, u32) -> i32,
clear() -> ());
Here, we generate a struct called MockBalanceSheet. This struct contains all the necessary data to store the number of types each method is called, what arguments they are invoked with and what values each method should return when invoked. This data is stored per-method, with the struct having a double::Mock field for each method. This is why all of the trait's methods must be declared when the struct is generated.
For step 2, we generate the bodies of the mocked methods. The generated bodies contain boilerplate code for passing the method's arguments to the underlying double::Mock objects using mock_method. For example:
impl BalanceSheet for MockBalanceSheet {
mock_method!(profit(&self, revenue: u32, costs: u32) -> i32);
mock_method!(clear(&mut self));
}
Notice how both immutable and mutable methods can be specified. One just passes
&selfor&mut selftomock_method, depending on whether thetraitbeing mocked specifies the method as immutable or mutable.
After both of these steps, the mock object is ready to use.
Tests with mocks are typically structured like so:
For example, suppose we wish to test some code that uses a BalanceSheet generate a HTML page showing the current profit of something:
fn generate_profit_page<T: BalanceSheet>(revenue: u32, costs: u32, sheet: &T) {
let profit_str = sheet.profit(revenue, costs).to_string();
return "<html><body>
Profit is: $" + profit_str + "
</body></html>";
}
We can use our generated MockBalanceSheet to test this function:
fn test_balance {
// GIVEN:
// create instance of mock and configure its behaviour (will return 42)
let mock = MockBalanceSheet::default();
mock.profit.return_value(42);
// WHEN:
// run code under test
let page = generate_profit_page(30, 20);
// THEN:
// ensure mock affected output in the right away
assert_eq!("<html><body>
Profit is: $42
</body></html>")
// also assert that the mock's profit() method was called _exactly_ once,
// with the arguments 30 (for revenue) and 20 (for costs).
assert_true!(mock.profit.has_calls_exactly(
vec!((30, 20))
));
}
Mocks can be configured to return a single value, a sequence of values (one value for each call) or invoke a function/closure. Additionally, it is possible to make a mock return special values/invoke special functions when specific arguments are passed in.
These behaviours are configured by invoking methods on the mock objects themselves. These methods are listed in the table below.
| Method | What It Does |
|---|---|
use_fn_for((args), dyn Fn(...) -> retval) |
invoke given function and return the value it returns when specified (args) are passed in |
use_closure_for((args), &dyn Fn(...) -> retval) |
invoke given closure and return the value it returns when specified (args) are passed in |
return_value_for((args), val) |
return val when specified (args) are passed in |
use_fn(dyn Fn(...) -> retval) |
invoke given function and return the value it returns by default |
use_closure(&dyn Fn(...) -> retval) |
invoke given closure and return the value it returns by default |
return_values(vec<retval>) |
return values in given vector by default, return one value for each invocation of the mock method. If there are no more values in the vector, return the default value specified by return_value() |
return_value(val) |
return val by default |
If no behaviour is specified, the mock will just return the default value of the return type, as specified by the Default trait.
Example usage:
// Configure mock to return 9001 profit when given args 42 and 10. Any other
// arguments will cause the mock to return a profit of 1.
let sheet = MockBalanceSheet::default();
sheet.profit.return_value_for((42, 10), 9001);
sheet.profit.return_value(1);
// Configure mock to call arbitrary function. The mock will return the
// result of the function back to the caller.
fn subtract(revenue: u32, costs: u32) -> i32 {
revenue - costs
}
let sheet2 = MockBalanceSheet::default();
sheet.use_fn(subtract);
Code examples on how to use these are available in the rustdocs.
It is possible to use many of these in conjunction. For example, one can tell a mock to return a specific value for args (42, 10) using return_value_for(), but return the default value of 1 for everything else using return_value().
When a mock method is invoked, it uses a precdence order to determine if it should return a default value, return a specific value, invoke a function and so on.
The precedence order of these methods is the same order they are specified in the above table. For example, if use_fn and return_value are invoked, then the mock will invoke the function passed to use_fn and not return a value.
If a method returns an Option<T> or a Result<T, E>, then one can use the following convenience functions for specifying default return values:
| Method | Returns | What It Does |
|---|---|---|
return_some |
Some(val) |
return Some(val) enum of Option |
return_none |
None |
returs the None enum of Option |
return_ok |
Ok(val) |
return Ok(val) enum of Result |
return_err |
Err(val) |
return Err(val) enum of Result |
After the test has run, we can verify the mock was called the right number of times and with the right arguments.
The table below lists the methods that can be used to verify the mock was invoked as expected.
| Method | Returns | What It Does |
|---|---|---|
calls() |
Vec<(Args)> |
return the arguments of each mock invocation, ordered by invocation time. |
called() |
bool |
return true if method was called at least once. |
num_calls() |
usize |
number of times method was called. |
called_with((args)) |
bool |
return true if method was called at least once with given args. |
has_calls(vec!((args), ...)) |
bool |
return true if method was called at least once for each of the given args tuples. |
has_calls_in_order(vec!((args), ...)) |
bool |
return true if method was called at least once for each of the given args collections, and called with arguments in the same order as specified in the input vec. |
has_calls_exactly(vec!((args), ...)) |
bool |
return true if method was called exactly once for each of the given args collections. |
has_calls_exactly_in_order(vec!((args), ...)) |
bool |
return true if method was called exactly once for each of the given args collections, and called with arguments in the same order as specified in the input vec. |
called_with_pattern(matcher_set) |
bool |
return true if method was called at least once with args that match the given matcher set. |
has_patterns(vec!(matcher_set, ...)) |
bool |
return true if all of the given matcher sets were matched at least once by the mock's calls. |
has_patterns_in_order(vec!(matcher_set, ...)) |
bool |
return true if mock has calls that match all the specified matcher sets. The matcher sets must be matched in the order they are specified by the input matcher_set vector. |
has_patterns_exactly(vec!(matcher_set, ...)) |
bool |
return true if all of the given matcher sets were matched at least once by the mock's calls. The number of calls equal the number of specified matcher sets. |
has_patterns_exactly_in_order(vec!(matcher_set, ...)) |
bool |
return true if mock has calls that match all the specified matcher sets. The matcher sets must be matched in the order they are specified by the input matcher_set vector. T the number of calls equal the number of specified matcher sets. |
Example usage:
let sheet = MockBalanceSheet::default();
// invoke mock method
sheet.profit(42, 10);
sheet.profit(5, 0);
// assert the invocation was recorded correctly
assert!(sheet.profit.called());
assert!(sheet.profit.called_with((42, 10)));
assert!(sheet.profit.has_calls((42, 10)));
assert!(sheet.profit.has_calls_in_order((42, 10), (5, 0)));
assert!(sheet.profit.has_calls_exactly((5, 0), (42, 10)));
assert!(sheet.profit.has_calls_exactly_in_order((42, 10), (5, 0)));
See section Pattern Matching for detail on how to use the pattern-based assertions.
Invoke reset_calls() to clear all recorded calls of a mock method.
To ensure individual tests are as isolated (thus, less likely to have bugs) as possible, it is recommended that different mock objects are constructed for different test cases.
Nevertheless, there might a some case where reusing the same mock and its return values results in easier to read and more maintainable test code. In those cases, reset_calls() can be used to clear calls from previous tests.
When a mock function has been used in a test, we typically want to make assertions about what the mock has been called with. For example, suppose we're testing some logic that determines the next action of a robot. We might want to assert what this logic told the robot to do:
let robot = MockRobot::default();
do_something_with_the_robot(&robot);
assert!(robot.move_forward.called_with(100);
The above code checks that do_something_with_the_robot() should tell the robot to move 100 units forward. However, sometimes you might not want to be this specific. This can make tests being too rigid. Over specification leads to brittle tests and obscures the intent of tests. Therefore, it is encouraged to specify only what's necessary — no more, no less.
If you care that moved_forward() will be called but aren't interested in its actual argument, you can simply assert on the call count:
assert!(robot.move_forward.called())
assert!(robot.move_forward.num_calls() == 1u)
But what if the behaviour we wanted to check is a little more nuanced? What if we wanted to check that the robot was moved forward at least 100 units, but it didn't matter if the robot moved even further than that? If this case, our assertion is more specific than "was move_forward() called?", but the constraint is not as tight as "has to be moved exactly 100 units".
If we know the current implementation will move exactly 100 units, it is tempting to just use check for exact equality. However, as mentioned, this makes the tests very brittle. If the implementation is technically free to change, and start moving more than 100 units, then this test breaks. The developer has to go to the tests and fix the broken test. That change would not be required if the test wasn't overly restrictive. This may sound minor. However, code bases grow. This means the number of tests also grows. If all of the tests are brittle,
$ claude mcp add double \
-- python -m otcore.mcp_server <graph>