Getting Started
faux
makes liberal use of unsafe Rust features, so it is only
recommended for use inside tests. Follow the steps below to configure
faux
and the created mocks to only exist during tests.
Installation
faux
should be added under [dev-dependencies]
in Cargo.toml
.
[dev-dependencies]
faux = "^0.1"
This makes sure that faux
only gets included when compiling and
running tests, thus making it impossible to leak into production code.
Your First Mock
faux
is able to mock a struct
and its public methods. To do this,
faux
provides two attributes: #[faux::create]
and
#[faux::methods]
. #[faux::create]
tags the struct we wish to make
mockable. #[faux::methods]
tags the impl
blocks of that
struct. Both of these attributes must be used.
#[cfg_attr(test, faux::create)] pub struct MyStructToMock { /* fields */ } #[cfg_attr(test, faux::methods)] impl MyStructToMock { /* methods to mock */ } fn main() {}
Example
Let's say you are writing a restaurant reservation system. One of the
core structs in this system is a RestaurantClient
which sends HTTP
requests to get availability times for a restaurant, create a
reservation, cancel, etc.
pub struct RestaurantClient { /* snip */ } impl RestaurantClient { pub fn new() -> Self { /* snip */ todo!() } pub fn availabilities(&self) -> Result<Vec<Availability>, Error> { /* GET to some HTTP endpoint */ todo!() } pub fn reserve(&self, availability: Availability) -> Result<Reservation, Error> { /* POST to some HTTP endpoint */ todo!() } pub fn cancel(&self, reservation: Reservation) -> Result<(), Error> { /* DELETE to some HTTP endpoint */ todo!() } } pub struct Reservation { /* snip */ } pub struct Availability { /* snip */ } pub struct Error { /* snip */ } fn main() {}
This type is not interesting to unit-test in itself as it is very declarative. Aside from the fact that it doesn't have any real logic to unit-test, calling these methods will send actual HTTP requests to create or cancel reservations, which is bound to make your tests slow and flaky. You will also probably have some really angry restaurants.
You may want to have some kind of integration or enemy tests that verifies the overall correctness of your service that will end up testing this struct, but that goes beyond the scope of this guide.
However, a more interessting part of your library deals with
choosing from the possible availabilities and reserves a spot at the
restaurant. Let's call it Concierge
.
use restaurant_client::{RestaurantClient, Reservation}; pub struct Concierge { client: RestaurantClient, } impl Concierge { pub fn new(client: RestaurantClient) -> Self { Concierge { client, } } pub fn reserve_matching(&self, options: Options) -> Result<Reservation, Error> { /* logic to find a matching availability and reserve it */ todo!() } } pub struct Options { /* snip */ } pub enum Error { Client(restaurant_client::Error), } impl From<restaurant_client::Error> for Error { fn from(error: restaurant_client::Error) -> Self { Error::Client(error) } } mod restaurant_client { pub struct RestaurantClient {} impl RestaurantClient { pub fn availabilities(&self) -> Result<Vec<Availability>> { todo!() } pub fn reserve(&self, availability: Availability) -> Result<Reservation> { todo!() } } pub struct Reservation { /* snip */ } pub struct Availability { /* snip */ } pub struct Error { /* snip */ } pub type Result<T> = std::result::Result<T, Error>; } fn main() {}
Unlike ReservationClient
, the Concierge
does hold a key piece of
domain logic: how to choose between the available times. This logic is
worth unit tests as it is vital to our service and we want to make
sure that it continues working as we refactor or add features to
reserve_matching
. However, we do not want to make actual calls to
the ReservationClient
as that would mean having to make network
requests. To solve this, we decide to mock ReservationClient
for our
tests. faux
makes it easy to make this struct mockable using the
faux::create
and faux::methods
attributes.
// gate the attribute to only tests // `faux` is (and should be!) only available when running tests #[cfg_attr(test, faux::create)] pub struct RestaurantClient { /* snip */ } // gate the attribute to only tests #[cfg_attr(test, faux::methods)] impl RestaurantClient { pub fn new() -> Self { /* snip */ todo!() } pub fn availabilities(&self) -> Result<Vec<Availability>, Error> { /* snip */ todo!() } pub fn reserve(&self, availability: Availability) -> Result<Reservation, Error> { /* snip */ todo!() } pub fn cancel(&self, reservation: Reservation) -> Result<(), Error> { /* snip */ todo!() } } pub struct Reservation { /* snip */ } pub struct Availability { /* snip */ } pub struct Error { /* snip */ } fn main() {}
Using these two attributes allows/signals faux
to hook into the
struct and its methods at compile time to create mockable versions of
them that can be used in your tests. Note that there are zero changes
to the implementation or signature of ReservationClient
, the only
change is tagging it with the faux
attributes.
use restaurant_client::{RestaurantClient, Reservation}; pub struct Concierge { client: RestaurantClient, } impl Concierge { pub fn new(client: RestaurantClient) -> Self { Concierge { client, } } pub fn reserve_matching(&self, options: Options) -> Result<Reservation, Error> { let _ = options; let chosen_availability = self.client .availabilities()? .pop() .ok_or(Error::NoReservations)?; let reservation = self.client.reserve(chosen_availability)?; Ok(reservation) } } pub struct Options { /* snip */ } #[derive(Clone, Debug)] pub enum Error { Client(restaurant_client::Error), NoReservations, } impl From<restaurant_client::Error> for Error { fn from(error: restaurant_client::Error) -> Self { Error::Client(error) } } mod restaurant_client { #[faux::create] pub struct RestaurantClient {} #[faux::methods] impl RestaurantClient { pub fn availabilities(&self) -> Result<Vec<Availability>> { todo!() } pub fn reserve(&self, availability: Availability) -> Result<Reservation> { todo!() } } #[derive(Clone, Debug, PartialEq)] pub struct Reservation { /* snip */ } #[derive(Clone, Debug, PartialEq)] pub struct Availability { /* snip */ } #[derive(Clone, Debug)] pub struct Error { /* snip */ } pub type Result<T> = std::result::Result<T, Error>; } extern crate faux; use faux::when; use restaurant_client::Availability; fn main() { // first test let mut client = RestaurantClient::faux(); let availability = Availability { /*snip */ }; let expected_reservation = Reservation { /* snip */ }; when!(client.availabilities()) .then_return(Ok(vec![availability.clone()])); when!(client.reserve(availability)) .then_return(Ok(expected_reservation.clone())); let subject = Concierge::new(client); let options = Options { /* snip */ }; let reservation = subject .reserve_matching(options).expect("expected successful reservation"); assert_eq!(reservation, expected_reservation); // second test let mut client = RestaurantClient::faux(); when!(client.availabilities()).then_return(Ok(vec![])); let subject = Concierge::new(client); let options = Options { /* snip */ }; let error = subject .reserve_matching(options) .expect_err("expected error reservation"); assert!(matches!(error, Error::NoReservations)); } #[cfg(test)] mod tests { use super::*; use faux::when; #[test] fn selects_the_only_one() { // A `faux()` function to every mockable struct // to instantiate a mock instance let mut client = RestaurantClient::faux(); let availability = Availability { /*snip */ }; let expected_reservation = Reservation { /* snip */ }; // when!(...) lets you stub the return method of the mock struct when!(client.availabilities()) .then_return(Ok(vec![availability.clone()])); // when!(...) lets you specify expected arguments // so only invocations that match that argument return the stubbed data when!(client.reserve(availability)) .then_return(Ok(expected_reservation.clone())); let subject = Concierge::new(client); let options = Options { /* snip */ }; let reservation = subject .reserve_matching(options) .expect("expected successful reservation"); assert_eq!(reservation, expected_reservation); } #[test] fn fails_when_empty() { let mut client = RestaurantClient::faux(); when!(client.availabilities()).then_return(Ok(vec![])); let subject = Concierge::new(client); let options = Options { /* snip */ }; let error = subject .reserve_matching(options) .expect_err("expected error reservation"); assert!(matches!(error, Error::NoReservations)); } }
You have now successfully added tests for Concierge
that use a mock
instance of the RestaurantClient
. Note that neither the
implementation of Concierge
nor RestaurantClient
had to change in
order to be mockable. You can write production ready code without
incurring any abstraction penalty for using mocks in testing.
Recap
-
Use
faux
as adev-dependency
to avoid it leaking into production code. -
faux::create
andfaux::methods
are attributes used to tag structs and methods for mocking. These tags should be gated to tests only using#[cfg_attr(test, ...)]
-
faux::when!
is used to stub the returned data of a method in a mocked struct. -
faux::when!
lets you specify argument matchers so stubs are used only for certain invocations. The default is an equality matcher, but there are also other matchers if you want to match any argument, match a pattern, or match based on the result of a given predicate. See the [when docs] for more information.