TESTING IN RUST A PRIMER IN TESTING AND MOCKING @donald_whyte - - PowerPoint PPT Presentation

testing in rust
SMART_READER_LITE
LIVE PREVIEW

TESTING IN RUST A PRIMER IN TESTING AND MOCKING @donald_whyte - - PowerPoint PPT Presentation

TESTING IN RUST A PRIMER IN TESTING AND MOCKING @donald_whyte FOSDEM 2018 ABOUT ME Soware Engineer @ Engineers Gate Real-time trading systems Scalable data infrastructure Python/C++/Rust developer MOTIVATION Rust focuses on memory


slide-1
SLIDE 1

TESTING IN RUST

A PRIMER IN TESTING AND MOCKING @donald_whyte

FOSDEM 2018

slide-2
SLIDE 2

Soware Engineer @ Engineers Gate Real-time trading systems Scalable data infrastructure Python/C++/Rust developer

ABOUT ME

slide-3
SLIDE 3

MOTIVATION

Rust focuses on memory safety. While supporting advanced concurrency. Does a great job at this.

slide-4
SLIDE 4

But even if our code is safe... ...we still need to make sure it's doing the right thing.

slide-5
SLIDE 5

OUTLINE

Rust unit tests Mocking in Rust using double Design considerations

slide-6
SLIDE 6
  • 1. UNIT TESTS
slide-7
SLIDE 7

Create library: cargo new

cargo new some_lib cd some_lib

slide-8
SLIDE 8

Test fixture automatically generated:

> cat src/lib.rs #[cfg(test)] mod tests { #[test] fn it_works() { // test code in here } }

slide-9
SLIDE 9

Write unit tests for a module by defining a private tests module in its source file.

// production code pub fn add_two(num: i32) ­> i32 { num + 2 } #[cfg(test)] mod tests { // test code in here }

slide-10
SLIDE 10

Add isolated test functions to private tests module.

// ...prod code... #[cfg(test)] mod tests { use super::*; // import production symbols from parent module #[test] fn ensure_two_is_added_to_negative() { assert_eq!(0, add_two(­2)); } #[test] fn ensure_two_is_added_to_zero() { assert_eq!(2, add_two(0)); } #[test] fn ensure_two_is_added_to_positive() { assert_eq!(3, add_two(1)); } }

slide-11
SLIDE 11

cargo test

user:some_lib donaldwhyte$ cargo test Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running target/debug/deps/some_lib­4ea7f66796617175 running 3 tests test tests::ensure_two_is_added_to_negative ... ok test tests::ensure_two_is_added_to_positive ... ok test tests::ensure_two_is_added_to_zero ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured

slide-12
SLIDE 12

Rust has native support for: documentation tests integration tests

slide-13
SLIDE 13
  • 2. WHAT IS MOCKING?
slide-14
SLIDE 14
slide-15
SLIDE 15
slide-16
SLIDE 16
slide-17
SLIDE 17
slide-18
SLIDE 18

WHAT TO ELIMINATE

Anything non-deterministic that can't be reliably controlled within a unit test.

slide-19
SLIDE 19

External data sources — files, databases Network connections — services External code dependencies — libraries

slide-20
SLIDE 20

CAN ALSO ELIMINATE

Large internal dependencies for simpler tests.

slide-21
SLIDE 21

SOLUTION: USE TEST DOUBLE

Term originates from a notion of a "stunt double" in films.

slide-22
SLIDE 22

A test double is an object or function substituted for production code during testing. Should behave in the same way as the production code. Easier to control for testing purposes.

slide-23
SLIDE 23

Many types of test double: Stub Spy Mock Fake They're oen all just referred to "mocks".

slide-24
SLIDE 24

Spies are used in this talk.

slide-25
SLIDE 25

SPIES PERFORM BEHAVIOUR VERIFICATION

Tests code by asserting its interaction with its collaborators.

slide-26
SLIDE 26
  • 3. TEST DOUBLES IN RUST

USING DOUBLE

slide-27
SLIDE 27

double generates mock implementations for: traits functions

slide-28
SLIDE 28

Flexible configuration of a double's behaviour. Simple and complex assertions on how mocks were used/called.

slide-29
SLIDE 29

EXAMPLE

Predicting profit of a stock portfolio over time.

slide-30
SLIDE 30

COLLABORATORS

pub trait ProfitModel { fn profit_at(&self, timestamp: u64) ­> f64; }

slide-31
SLIDE 31

IMPLEMENTATION

pub fn predict_profit_over_time<M: ProfitModel>( model: &M, start: u64, end: u64) ­> Vec<f64> { (start..end + 1) .map(|t| model.profit_at(t)) .collect() }

slide-32
SLIDE 32

We want to test predict_profit_over_time().

slide-33
SLIDE 33

Tests should be repeatable. Not rely on an external environment.

slide-34
SLIDE 34

One collaborator — ProfitModel.

slide-35
SLIDE 35

PREDICTING PROFIT IS HARD

Real ProfitModel implementations use: external data sources (DBs, APIs, files) complex internal code dependencies (math models)

slide-36
SLIDE 36

Let's mock ProfitModel.

slide-37
SLIDE 37

mock_trait! Generate mock struct that records interaction:

pub trait ProfitModel { fn profit_at(&self, timestamp: u64) ­> f64; } mock_trait!( MockModel, profit_at(u64) ­> f64);

slide-38
SLIDE 38

mock_trait!

mock_trait!( NameOfMockStruct, method1_name(arg1_type, ...) ­> return_type, method2_name(arg1_type, ...) ­> return_type ... methodN_name(arg1_type, ...) ­> return_type);

slide-39
SLIDE 39

mock_method! Generate implementations of all methods in mock struct.

mock_trait!( MockModel, profit_at(u64) ­> f64); impl ProfitModel for MockModel { mock_method!(profit_at(&self, timestamp: u64) ­> f64); }

slide-40
SLIDE 40

mock_method!

impl TraitToMock for NameOfMockStruct { mock_method!(method1_name(&self, arg1_type, ...) ­> return_type); mock_method!(method2_name(&self, arg1_type, ...) ­> return_type); ... mock_method!(methodN_name(&self, arg1_type, ...) ­> return_type); }

slide-41
SLIDE 41

Full code to generate a mock implementation of a trait:

mock_trait!( MockModel, profit_at(u64) ­> f64); impl ProfitModel for MockModel { mock_method!(profit_at(&self, timestamp: u64) ­> f64); }

slide-42
SLIDE 42

USING GENERATED MOCKS IN TESTS

#[test] fn test_profit_model_is_used_for_each_timestamp() { // GIVEN: let mock = MockModel::default(); mock.profit_at.return_value(10); // WHEN: let profit_over_time = predict_profit_over_time(&mock, 0, 2); // THEN: assert_eq!(vec!(10, 10, 10), profit_over_time); assert_eq!(3, model.profit_at.num_calls()); }

slide-43
SLIDE 43

GIVEN: SETTING MOCK BEHAVIOUR

slide-44
SLIDE 44

DEFAULT RETURN VALUE

#[test] fn no_return_value_specified() { // GIVEN: let mock = MockModel::default(); // WHEN: let profit_over_time = predict_profit_over_time(&mock, 0, 2); // THEN: // default value of return type is used if no value is specified assert_eq!(vec!(0, 0, 0), profit_over_time); }

slide-45
SLIDE 45

ONE RETURN VALUE FOR ALL CALLS

#[test] fn single_return_value() { // GIVEN: let mock = MockModel::default(); mock.profit_at.return_value(10); // WHEN: let profit_over_time = predict_profit_over_time(&mock, 0, 2); // THEN: assert_eq!(vec!(10, 10, 10), profit_over_time); }

slide-46
SLIDE 46

SEQUENCE OF RETURN VALUES

#[test] fn multiple_return_values() { // GIVEN: let mock = MockModel::default(); mock.profit_at.return_values(1, 5, 10); // WHEN: let profit_over_time = predict_profit_over_time(&mock, 0, 2); // THEN: assert_eq!(vec!(1, 5, 10), profit_over_time); }

slide-47
SLIDE 47

RETURN VALUES FOR SPECIFIC ARGS

#[test] fn return_value_for_specific_arguments() { // GIVEN: let mock = MockModel::default(); mock.profit_at.return_value_for((1), 5); // WHEN: let profit_over_time = predict_profit_over_time(&mock, 0, 2); // THEN: assert_eq!(vec!(0, 5, 0), profit_over_time); }

slide-48
SLIDE 48

USE CLOSURE TO COMPUTE RETURN VALUE

#[test] fn using_closure_to_compute_return_value() { // GIVEN: let mock = MockModel::default(); mock.profit_at.use_closure(|t| t * 5 + 1); // WHEN: let profit_over_time = predict_profit_over_time(&mock, 0, 2); // THEN: assert_eq!(vec!(1, 6, 11), profit_over_time); }

slide-49
SLIDE 49

THEN: CODE USED MOCK AS EXPECTED

Verify mocks are called: the right number of times with the right arguments

slide-50
SLIDE 50

ASSERT CALLS MADE

#[test] fn asserting_mock_was_called() { // GIVEN: let mock = MockModel::default(); // WHEN: let profit_over_time = predict_profit_over_time(&mock, 0, 2); // THEN: // Called at least once. assert!(mock.profit_at.called()); // Called with argument 1 at least once. assert!(mock.profit_at.called_with((1))); // Called at least once with argument 1 and 0. assert!(mock.profit_at.has_calls((1), (0))); }

slide-51
SLIDE 51

TIGHTER CALL ASSERTIONS

#[test] fn asserting_mock_was_called_with_precise_constraints() { // GIVEN: let mock = MockModel::default(); // WHEN: let profit_over_time = predict_profit_over_time(&mock, 0, 2); // THEN: // Called exactly three times, with 1, 0 and 2. assert!(mock.profit_at.has_calls_exactly((1), (0), (2))); // Called exactly three times, with 0, 1 and 2 (in that order). assert!(mock.profit_at.has_calls_exactly_in_order( (0), (1), (2) )); }

slide-52
SLIDE 52

MOCKING FREE FUNCTIONS

Useful for testing code that takes function objects for runtime polymorphism.

slide-53
SLIDE 53

mock_func!

fn test_input_function_called_twice() { // GIVEN: mock_func!(mock, // variable that stores mock object mock_fn, // variable that stores closure i32, // return value type i32); // argument 1 type mock.return_value(10); // WHEN: code_that_calls_func_twice(&mock_fn); // THEN: assert_eq!(2, mock.num_calls()); assert!(mock.called_with(42)); }

slide-54
SLIDE 54
  • 4. PATTERN MATCHING
slide-55
SLIDE 55

ROBOT DECISION MAKING

slide-56
SLIDE 56

Actuator Robot WorldState

WorldState Struct containing current world state Robot Processes state of the world and makes decisions on what do to next. Actuator Manipulates the world. Used by Robot to act on the decisions its made.

slide-57
SLIDE 57

TEST THE ROBOT'S DECISIONS

Actuator Robot WorldState

slide-58
SLIDE 58

TEST THE ROBOT'S DECISIONS

Mock Actuator Robot WorldState

slide-59
SLIDE 59

COLLABORATORS

pub trait Actuator { fn move_forward(&mut self, amount: i32); // ... }

slide-60
SLIDE 60

GENERATE MOCK COLLABORATORS

mock_trait!( MockActuator, move_forward(i32) ­> ()); impl Actuator for MockActuator { mock_method!(move_forward(&mut self, amount: i32)); }

slide-61
SLIDE 61

IMPLEMENTATION

pub struct Robot<A> { actuator: &mut A } impl<A: Actuator> Robot { pub fn new(actuator: &mut A) ­> Robot<A> { Robot { actuator: actuator } } pub fn take_action(&mut self, state: WorldState) { // Complex business logic that decides what actions // the robot should take. // This is what we want to test. } } }

slide-62
SLIDE 62

TESTING THE ROBOT

#[test] fn test_the_robot() { // GIVEN: let input_state = WorldState { ... }; let actuator = MockActuator::default(); // WHEN: { let robot = Robot::new(&actuator); robot.take_action(input_state); } // THEN: assert!(actuator.move_forward.called_with(100)); }

slide-63
SLIDE 63

Do we really care that the robot moved exactly 100 units?

slide-64
SLIDE 64

All Possible Behaviour

slide-65
SLIDE 65

All Possible Behaviour Expected

slide-66
SLIDE 66

All Possible Behaviour Expected Asserted

slide-67
SLIDE 67

All Possible Behaviour Expected

Behaviour changes!

Asserted

slide-68
SLIDE 68

All Possible Behaviour Expected + Asserted

slide-69
SLIDE 69

Behaviour verification can overfit the implementation. Lack of tooling makes this more likely.

slide-70
SLIDE 70

PATTERN MATCHING TO THE RESCUE

slide-71
SLIDE 71

Match argument values to patterns. Not exact values. Loosens test expectations, making them less brittle.

slide-72
SLIDE 72

called_with_pattern()

#[test] fn test_the_robot() { // GIVEN: let input_state = WorldState { ... }; let actuator = MockActuator::default(); // WHEN: { let robot = Robot::new(&actuator); robot.take_action(input_state); } // THEN: let is_greater_or_equal_to_100 = |arg: &i32| *arg >= 100; assert!(actuator.move_forward.called_with_pattern( is_greater_than_or_equal_to_100 )); }

slide-73
SLIDE 73

Parametrised matcher functions:

/// Matcher that matches if `arg` is greater than or /// equal to `base_val`. pub fn ge<T: PartialEq + PartialOrd>( arg: &T, base_val: T) ­> bool { *arg >= base_val }

slide-74
SLIDE 74

Use p! to generate matcher closures on-the-fly.

use double::matcher::ge; let is_greater_or_equal_to_100 = p!(ge, 100);

slide-75
SLIDE 75

use double::matcher::*; #[test] fn test_the_robot() { // GIVEN: let input_state = WorldState { ... }; let actuator = MockActuator::default(); // WHEN: { let robot = Robot::new(&actuator); robot.take_action(input_state); } // THEN: assert!(actuator.move_forward.called_with_pattern( p!(ge, 100) )); }

slide-76
SLIDE 76

BUILT-IN MATCHERS

slide-77
SLIDE 77

WILDCARD any() argument can be any value of the correct type

slide-78
SLIDE 78

COMPARISON MATCHERS

eq(value) argument == value ne(value) argument != value lt(value) argument < value le(value) argument <= value gt(value) argument > value ge(value) argument >= value is_some(matcher) arg is Option::Some, whose contents matches matcher is_ok(matcher) arg is Result::Ok, whose contents matches matcher is_err(matcher) arg is Result::er, whose contents matches matcher

slide-79
SLIDE 79

FLOATING-POINT MATCHERS

f32_eq(value) argument is a value approximately equal to the f32 value, treating two NaNs as unequal. f64_eq(value) argument is a value approximately equal to the f64 value, treating two NaNs as unequal. nan_sensitive_f32_eq(value) argument is a value approximately equal to the f32 value, treating two NaNs as equal. nan_sensitive_f64_eq(value) argument is a value approximately equal to the f64 value, treating two NaNs as equal.

slide-80
SLIDE 80

STRING MATCHERS

has_substr(string) argument contains string as a sub-string. starts_with(prefix) argument starts with string prefix. ends_with(suffix) argument ends with string suffix. eq_nocase(string) argument is equal to string, ignoring case. ne_nocase(value) argument is not equal to string, ignoring case.

slide-81
SLIDE 81

CONTAINER MATCHERS

is_empty argument implements IntoIterator and contains no elements. has_length(size_matcher) argument implements IntoIterator whose element count matches size_matcher. contains(elem_matcher) argument implements IntoIterator and contains at least

  • ne element that matches elem_matcher.

each(elem_matcher) argument implements IntoIterator and all of its elements match elem_matcher. unordered_elements_are(elements) argument implements IntoIterator that contains the same elements as the vector elements (ignoring order). when_sorted(elements) argument implements IntoIterator that, when its elements are sorted, matches the vector elements.

slide-82
SLIDE 82

COMPOSITE MATCHERS

Assert that a single arg should match many patterns.

// Assert robot moved between 100 and 200 units. assert!(robot.move_forward.called_with_pattern( p!(all_of, vec!( p!(ge, 100), p!(le, 200) )) ));

slide-83
SLIDE 83

COMPOSITE MATCHERS

Assert all elements of a collection match a pattern:

let mock = MockNumberRecorder::default(); mock.record_numbers(vec!(42, 100, ­49395, 502)); // Check all elements in passed in vector are non­zero. assert!(mock.record_numbers.called_with_pattern( p!(each, p!(ne, 0)) ));

slide-84
SLIDE 84

CUSTOM MATCHERS

Define new matchers if the built-in ones aren't enough.

fn custom_matcher<T>(arg: &T, params...) ­> bool { // matching code here }

slide-85
SLIDE 85
  • 5. DESIGN CONSIDERATIONS
slide-86
SLIDE 86

2 design goals in double.

slide-87
SLIDE 87
  • 1. RUST STABLE FIRST
slide-88
SLIDE 88
  • 2. NO CHANGES TO PRODUCTION CODE

REQUIRED

Allows traits from the standard library or external crates to be mocked.

slide-89
SLIDE 89

CHALLENGING

Meeting these goals is difficult, because Rust: is a compiled/statically typed language runs a borrow checker

slide-90
SLIDE 90

Most mocking libraries require nightly. Most (all?) mocking libraries require prod code changes.

slide-91
SLIDE 91

THE COST

double achieves the two goals at a cost. Longer mock definitions.

slide-92
SLIDE 92

FIN

slide-93
SLIDE 93

Mocking is used to isolate unit tests from exernal resources

  • r complex dependencies.

Achieved in Rust by replacing traits and functions.

slide-94
SLIDE 94

Behaviour verification can overfit implementation. Pattern matching expands asserted behaviour space to reduce overfitting.

slide-95
SLIDE 95

double is a crate for generating trait/function mocks. Wide array of behaviour setups and call assertions. First-class pattern matching support. Requires no changes to production code.

slide-96
SLIDE 96

ALTERNATIVE MOCKING LIBRARIES

mockers mock_derive galvanic-mock mocktopus

slide-97
SLIDE 97

LINKS

these slides: double repository: double documentation: example code from this talk: http://donso.io/mocking-in-rust-using-double https://github.com/DonaldWhyte/double https://docs.rs/double/0.2.2/double/ https://github.com/DonaldWhyte/mocking-in-rust-using- double/tree/master/code

slide-98
SLIDE 98

GET IN TOUCH

don@donso.io @donald_whyte https://github.com/DonaldWhyte

slide-99
SLIDE 99

APPENDIX

slide-100
SLIDE 100

IMAGE CREDITS

Gregor Cresnar Zurb Freepik Dave Gandy Online Web Fonts