Rust Design-for-Testability: a survey
What can we do when designing Rust code to make it easier to test? This is a survey of everything I could find1 about testing Rust with a particular focus on design for testability for correctness. Some of the articles show multiple things to do on a worked example, some are more focused on a particular trick.
There doesn’t seem to be a single place that describes all the testing ideas: it is scattered across book chapters, blog articles, medium articles, etc. but here are the main sources that I have found.
- The Rust book chapter on testing
- Testing command line applications in Rust (significant overlap with the Rust book)
- How to organize your Rust tests Probably the most exhaustive / thorough
- Testing sync at Dropbox – what they did in their Rust rewrite to improve testability
- Rust by example book chapter on testing
- Writing fuzzable code – John Regehr’s blog (not specifically about Rust)
- Testable Component Design in Rust
- Rust 2020: Testing
- How to use the Rust compiler as your integration testing framework
- Awesome Rust: Testing (collection of links)
- Writing Correct, Readable and Performant (CRaP) Rust code
- Rust testing tricks
- Awesome Rust: testing tools/libraries
- A practical test pyramid – Martin Fowler’s blog (not specifically about Rust)
I am not going to try to summarize what these sources say: the rest of this post is a list of some common / interesting topics and which of these sources describe it in more detail.
Although my main focus is on how to write Rust programs so that they are easy to test, I touch on the other half of the problem: how to do the testing.
[I am new to Rust and my own testing habits are somewhat ad-hoc so this is definitely not a recommendation of how to write software by me. I hope it is useful and that you will tell me what I have missed on twitter or by email so that I can update this post. I would love to hear about any team that has published recommendations for design-for-testability.]
Design techniques for improving testability
The sources listed above have a bunch of common suggestions that I explore in more detail below. Many of the sources I found have great discussions so I will not try to repeat their explanations in this document but will link to some of the better discussions of each idea that I found.
Use intermediate data structures
See: Testing sync, Rust book, Testing CLI applications, use the Rust compiler for integration testing
- Use intermediate data structures to separate deciding what to do from performing the action: allowing tests to check that the right decision is being made and avoiding the need to mock/fake the filesystem, etc.
- Aggressively use newtypes, structs, enums
- Parse and validate inputs early (eg convert strings to enums)
- Also, #[must_use], parse/validate early
- Writing I/O-Free (Sans-I/O) Protocol Implementations (not Rust specific)
Abstract testable code into separate functions
See: Rust book, Testing CLI applications
- Cleanly separate command line parsing from code that implements functionality
Abstract I/O and state side effects out of functions
See: Testing CLI applications, Writing fuzzable code, Testing sync
- Use of std::io::Write trait and writeln! instead of println! (and handle resulting potential error)
Avoid / reduce non-determinism
See: Testing sync, Writing fuzzable code
- Use Futures with a custom executor to eliminate the non-determinism of threads
- All randomized testing systems should be fully deterministic and easily reproducible
- Beware of additional randomness in libraries: e.g., Rust’s HashMap uses randomized hashing to protect against denial of service attacks. This makes testing harder.
- Determinism enables minimization of random tests (cf. proptest)
Defining correct behavior
- Oracles for random testing (not Rust specific)
- Use Function inverse pairs (eg print/parse functions)
- Compare two implementations
- Use asserts liberally
- Turn on sanitizers
- Contracts.rs / (crates.io link) – code contract library
- [inactive?] libhoare compiler plugin
Dependency injection and mock testing
See: Rust 2020: Testing, Testable Component Design in Rust
- Abstraction can be based on Higher order functions or objects
- Traits and mockall
- Module mocks using mocktopus
- shaku is a compile-time dependency injection library that works well with mockall
API design
See: Testing CLI applications, Rust API guidelines, If you use Unsafe, …, Testable Component Design, Writing Correct, Readable and Performant (CRaP)
API design strongly affects testability of that API
- The component should provide a stable contract composed of traits, structs, and enums
- Always implement Clone and fmt::Debug for public types
- types like failure::Error should be converted to something that is cloneable
-
Use ‘newtype’ to let the type system statically test for errors and use one of these to test that this is done correctly
- compiletest.rs for testing Rust compilations
- trybuild for testing error messages (eg from proc-macros)
- lang_tester for testing compilations including, but not limited to, Rust
- the fuzzy text matcher fm
- Use #![warn(missing_doc_code_examples)] (and other Rust API guidelines)
Writing tests
See: How to organize your Rust tests, Testing CLI applications, Rust by example,
Having structured your software to enable tests, there are a lot of different tools and libraries to support writing tests.
- Documentation tests
- How to organize your Rust tests
- Fuzzing book describes use of
- Use a generative testing / property-based testing crate such as
- QuickCheck
- has its own arbitrary implementation
- proptest
- has its own arbitrary implementation
- QuickCheck
- Unit tests vs integration tests
- Using assert_cmd crate to test applications (link has links to other useful crates)
- Better error reporting using one of
- anyhow crate – adding context to error messages
- color eyre crate – extends anyhow with .suggestion() and other context
- Use the {:?} and {:x?} Debug string formats in test harnesses (see std/fmt)
- Rutenspitz: procedural macros for testing (fuzzing) equivalence of two stateful models (e.g., data structures)
Test / Behavior driven design (TDD and BDD)
See: Rust testing tricks, From @test to #[test]: Java to Rust
Obviously, there are many, many articles about TDD, BDD, Agile, etc. in the context of Java and other OO languages. The following links are Rust specific but they are a bit random and need to be improved.
- Laboratory.rs – BDD-inspired test library (todo: are other BDD libraries maybe more popular?)
- Speculate RSpec inspired testing library
- Fluent assertions: spectral (last updated 2017)
- TDD with Rust (2017) – a small example
- Regression testing (testing against a golden reference) using one of
Specific topics
Note: links in this section are more likely to be out of date.
Code coverage
- cov-mark crate – adding explicit coverage annotations to code (blog, blog)
- Code coverage tools and crates
- Measuring test coverage of Rust programs (I think it is now easier than in 2017?)
- Rust Code Coverage Guide: kcov + Travis CI + Codecov / Coveralls (2016)
Testing embedded systems
- Using
cargo test
for embedded testing withpanic-probe
- defmt, a highly efficient Rust logging framework for embedded devices: a deferred formatting library that encodes I/O over a hardware trace port to reduce binary size on embedded systems
- Test setup/teardown without a framework – using panic::catch_unwind
- RFC 2318: Custom test frameworks – now in unstable
- Writing an OS in Rust: testing (uses custom test frameworks)
- Utest (is this still active?)
Concurrency, futures and async
- Two easy ways to test async functions in Rust
- Async book – testing a web server
- Tokio-test for mocking AsyncRead/Write and tasks (docs)
- actix async example
- Our first integration test – Actix_rt::test based chapter in book Zero to production in Rust (book)
- Loom tests concurrent code by running it many times with all possible thread interleavings
Testing frameworks
- Serializing Rust tests (github) – annotations to prevent some tests being run in parallel
- Skeptic – run doctest-like tests on README.md
- Trust automated test runner: reruns tests when files change
- Test-case procedural macro to generate tests from test-case annotations
- Artifact (aka RST) – requirement tracking software where comments in code are linked (in lightweight way) to requirements, specs and tests in a markdown document
- Fake.rs – interesting #derive option to describe how to generate fake values for structs. Can this be adapted to specify invariants for legal values of a type?
Testing GUIs
- Gtk-rs testing: testing UIs by being able to send events to gtk and observe results
- Dinghy: testing iOS and Android: challenges when you don’t have a command line
Testing APIs
- Compiletest.rs – for testing compiler plugins and similar
- In particular, checking that type system (etc) rejects misuse of APIs: If you use unsafe …
- Rust API guidelines (not so much about testing here – but useful)
Mutation testing
- Mutagen – mutation testing tool implemented using procedural macros
Test generation
- Writing a testcase generator for a programming language Generate random wasm with “wasm-smith” in Rust
Mocking libraries
There are a lot of mocking / faking libraries – this is a limited list of what I found.
- Rust mock shootout: a fairly thorough survey of Rust mocking libraries. This lead to the development of mockall that is one of (the?) most popular mocking library.
- mockall mocking library
- mocktopus
- Httpmock
- Partial-io wraps Read/Write implementations, optional Future and quickcheck support
Error handling
See: CLI applications in Rust, Structuring and using errors in 2020
Not quite about testing – but semi-relevant.
- Use of ?
- Anyhow – adding context to error messages
- Error handling survey
Misc
- kruetz on reddit described how he checks that code in mdbooks compiles correctly
More information
My interest in Rust testing is a hope/belief that a good way of making formal verification more accessible to software developers is to build on what they are already familiar with: testing. I described this last month.
If you found this article interesting, you might also enjoy these related posts
Discussion of this post
Thanks
Thanks to the following people for adding to the above
- Christoph Siedentop (@chsiedentop) pointed me at
- Laurence Tratt (@laurencetratt) pointed me at
- @fitzgen pointed me at
- Mark Drobnak pointed me at
-
By “everything I could find”, I mean that I searched for terms relating to “design for test”, “testability”, “Rust”, etc. and followed links in other blogs. I also searched the archives of “This week in Rust” for the word “test” and followed any promising links. ↩
The opinions expressed are my own views and not my employer's.