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 a dev-dependency to avoid it leaking into production code.

  • faux::create and faux::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.