🎉 🎉 Introducing faux 🎉 🎉
What is faux?
faux
is a traitless Rust mocking framework for creating mock
objects out of user-defined structs.
Why mock?
Mock objects are test versions of objects that contain fake implementations of their behavior. For example, if your code accesses your file system, makes network requests, or performs other expensive actions, it is often useful to mock that behavior. Mocking can help make your tests into true unit tests that run quickly and produce the same result every time without relying on external dependencies. For a deeper dive into mocks, read this post.
Example
extern crate faux; #[cfg_attr(test, faux::create)] #[faux::create] pub struct NetworkClient { /* data here */ } #[cfg_attr(test, faux::methods)] #[faux::methods] impl NetworkClient { pub fn fetch_id_matching(&self, a: u32) -> i32 { /* does some complicated stuff, maybe network calls */ 5 } } struct Service { client: NetworkClient, } impl Service { fn do_stuff(&self) -> i32 { self.client.fetch_id_matching(3) } } #[cfg(test)] #[test] fn service_does_the_right_thing() { // creates a mocked NetworkClient let mut client = NetworkClient::faux(); faux::when!(client.fetch_id_matching).then(|i| { // we want to test do_stuff(), which should always call // fetch_id_matching with the input 3. assert_eq!(i, 3, "expected service to send '3'"); // mock fetch_id_matching to always return 10 10 }); // create your service using the mocked client // the service is the subject under test let subject = Service { client }; let id = subject.do_stuff(); assert_eq!(id, 10); } fn main() { // creates a mocked NetworkClient let mut client = NetworkClient::faux(); // mock fetch_id_matching faux::when!(client.fetch_id_matching).then(|i| { assert_eq!(i, 3, "expected service to send '3'"); 10 }); // create your service using the mocked client // the service is the subject under test let subject = Service { client }; let id = subject.do_stuff(); assert_eq!(id, 10); }
By adding faux
attributes, we have succesfully mocked
NetworkClient::fetch_id_matching
to instead call a closure specified
in the test code and always return 10. Unlike the real method, the
mock does not make a network request. Thus, our test remains
dependable, focused, and free from external dependencies.
faux
provides users with two attributes: #[faux::create]
and
#[faux::methods]
. #[faux::create]
is required on any struct that
needs to be mocked and #[faux::methods]
is required on its impl
block. faux
also provides a when!
macro to mock methods that
were made mockable by #[faux::methods]
. See the docs for more information.
How is faux different than ${existing mocking framework}?
DISCLAIMER: this section is based on the author's knowledge of Rust mocking frameworks as of January 2020. Apologies in advance for any frameworks that were overlooked.
Currently in Rust, mocking depends heavily on traits.
#![allow(unused)] fn main() { struct NetworkClient { /* data here */ } impl NetworkClient { fn fetch_id_matching(&self, a: u32) -> i32 { /* does some complicated stuff, maybe network calls */ 5 } } struct Service { client: NetworkClient, } impl Service { fn do_stuff(&self) -> i32 { self.client.fetch_id_matching(3) } } }
In the code snippet above, we want to test Service
to make sure it
does the right thing. However, we want to avoid the expensive work
done in fetch_id_matching
, since making a network call in our test
would be both slow and unreliable. This means we need two different
implementations of NetworkClient
: one for tests, and one for
production. Using a common trait for the two implementations, we could
write the following:
trait TNetworkClient { fn fetch_id_matching(&self, a: u32) -> i32; } struct NetworkClient { /* data here */ } impl TNetworkClient for NetworkClient { fn fetch_id_matching(&self, a: u32) -> i32 { /* does some complicated stuff, maybe network calls */ 5 } } struct Service<C: TNetworkClient> { client: C, } impl<C: TNetworkClient> Service<C> { fn do_stuff(&self) -> i32 { self.client.fetch_id_matching(3) } } #[cfg(test)] struct MockNetworkClient { mocked_fetch_id_matching_result: i32, mocked_fetch_id_matching_argument: std::cell::Cell<u32>, } #[cfg(test)] impl TNetworkClient for MockNetworkClient { fn fetch_id_matching(&self, a: u32) -> i32 { self.mocked_fetch_id_matching_argument.set(a); self.mocked_fetch_id_matching_result } } struct MockNetworkClient { mocked_fetch_id_matching_result: i32, mocked_fetch_id_matching_argument: std::cell::Cell<u32>, } impl TNetworkClient for MockNetworkClient { fn fetch_id_matching(&self, a: u32) -> i32 { self.mocked_fetch_id_matching_argument.set(a); self.mocked_fetch_id_matching_result } } #[cfg(test)] #[test] fn service_does_the_right_thing() { //creates a mocked NetworkClient let client = MockNetworkClient { mocked_fetch_id_matching_argument: std::cell::Cell::default(), mocked_fetch_id_matching_result: 10, }; // create your service using the mocked client // the service is the subject under test let subject = Service { client }; let id = subject.do_stuff(); assert_eq!(id, 10); } fn main() { //creates a mocked NetworkClient let client = MockNetworkClient { mocked_fetch_id_matching_argument: std::cell::Cell::default(), mocked_fetch_id_matching_result: 10, }; // create your service using the mocked client // the service is the subject under test let subject = Service { client }; let id = subject.do_stuff(); assert_eq!(id, 10); }
Unfortunately, we have now changed our production code to
accommodate our tests, not because this is a better design but
because of testing requirements. Tests should guide the design of
your code without forcing undue complexity that only benefits the
tests. Now, every user of Service
needs to explicitly call out the
TNetworkClient
trait, thus cluttering the function/struct signature
of anything dealing with Service
. Furthermore, the TNetworkClient
trait is an unnecessary layer of abstraction for your production code,
which only uses one implementation of the trait.
While the code above is a simple example, imagine having to add mock interfaces to all the structs in a mature codebase. Most mocking frameworks for Rust are currently based on this approach. Although most can automatically generate the mock structs from traits, you still need to define hand-written traits for every mockable struct, and you still have to deal with generics and traits in your function/struct signatures.
faux
takes a different approach by transforming your struct and its
methods into mockable versions of themselves. These transformations
can be (and should be!) gated to only the test
cfg, thus having zero
impact on your production code.
Closing note
faux
is in a very early stage of development, and definitely does
not cover all the possibilities of a traitless mocking
framework. Thus, there is no guarantee of API stability between
releases, although every attempt will be made to keep the API
consistent. Please read the docs for the most up to date information
on faux
functionality.
See the issues in Github for an updated list of limitations and to get an idea of what might be coming next.
Feedback is always welcome, so feel free to open issues or send PRs.
A huge thanks to mocktopus, another traitless Rust mocking
framework, which was a huge inspiration behind the creation of faux
.