This guide is incomplete and in active development. Visit the API docs for an accurate view into faux

faux

faux is a library to create mocks out of structs.

With faux you can stub the behavior of your structs during tests, thus allowing you to write readable and maintainable unit tests.

Mocking is a powerful technique to write repeatable unit tests. Mocks let you focus on a particular system or component by mocking the dependencies underneath that may be hard or not valuable to setup in your unit tests. These kind of tests are not meant to replace higher level tests that would verify the correctness of your application as a whole.

This book is split into two sections: a guide for using faux, and an appendix of blog posts regarding faux.

More Information

For a deeper dive into the api of faux, you can read the API docs. faux is open source and it is available under an MIT License in github.

Motivation

faux was created with the purpose of simplifying mocking in Rust.

No undue abstractions

A typical technique for mocking in Rust is to use generics to provide a real implementation in production and a fake implementation in tests. Adding these generics create a layer of abstraction that is not necessery outside of tests. Abstractions are not free. Abstractions affect the readability and maintainability of your code so its cost needs to be outweighed by its benefits. In this case the benefit is only testability thus making it an undue burden.

It is the author's belief that writing traits solely for testing are an undue burden and create an unnecessary layer of abstraction.

In comparison, faux works by changing the implementation of a struct at compile time. These changes should be gated to only apply during tests, thus having zero effect in your production code. The goal of faux is to allow you to create mocks out of your existing code without forcing you write unnecessary abstractions.

Mocking behavior

faux is designed to mock visibile behavior. In Rust terms, faux is designed to mock pubic methods. Private methods are not visible and thus not mockable using faux. Fields of a struct are not behavior and thus not mockable using faux.

Free functions and associated functions are behavior but are not currently supported by faux. faux's current focus is object mocking, but functions may come in the future. Submit an issue if you wish to see function mocking so its relative priority can be known.

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.

Exporting Mocks Across Crates

As an application or library grows, it is common to split it into multiple crates. This separation of responsibilities help to simplify code, but there is a snag: mocks.

If any code is tagged wth the #[cfg(test)] attribute, Rust does not allow it to be exported outside its crate. This is great! We definitely do not want to be building or running someone else's tests when testing our crate. This means, however, that the mockable version of our structs are also not exported, as faux was gated to only work during tests.

This chapter explores a solution for exporting mocks across crates. Mocks can then be used within multiple crates of the same project, or even exposed to users of your library so they can mock your structs when testing their own library or application.

The solution explored in this chapter applies not only to faux but to any "test" code you want to export across crates.

To better explain, let's start with an example. Let's say we are building a graphics rendering library, testable-renderer. As expected, faux is declared in dev-dependencies

[package]
name = "testable-renderer"

[dev-dependencies]
faux = "^0.1"

And the code uses mocks:

extern crate faux;
#[faux::create]
#[cfg_attr(test, faux::create)]
pub struct Renderer {
    /* snip */
    _inner: u8,
}

#[faux::methods]
#[cfg_attr(test, faux::methods)]
impl Renderer {
    pub fn new() -> Renderer {
        /* snip */
        unimplemented!()
    }

    pub fn render(&mut self, texture: &Texture) -> Result<(), RenderError> {
        /* snip */
        unimplemented!()
    }
}

pub struct Texture;

impl Texture {
    pub fn render(&self, renderer: &mut Renderer) -> Result<(), RenderError> {
        renderer.render(self)
    }
}

#[derive(Debug)]
pub struct RenderError;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn renders_textures() {
        let mut renderer = Renderer::faux();
        faux::when!(renderer.render).then(|_| Ok(()));

        let subject = Texture {};
        subject.render(&mut renderer).expect("failed to render the texture")
    }
}

fn main() {
    let mut renderer = Renderer::faux();
    faux::when!(renderer.render).then(|_| Ok(()));
    let subject = Texture {};
    subject.render(&mut renderer).expect("failed to render the texture")
}

faux as a feature

For the mocks to be exported, they need to be built even outside of tests. However, we do not want to pollute the production builds of our library nor anyone using our library with faux and mocks, so we make our dependency on faux optional:

[dependencies]
# set up an optional feature outside of dev-dependencies so that users
# of this library can use our mocks in their own tests
faux = { version = "^0.1", optional = true }

[dev-dependencies]
# our tests still depend on faux; so add it again but do not make it
# optional
faux = "^0.1"

Note that we still include faux in dev-dependencies. Our tests are always dependent on faux, since they use mocks, so the dependency is not optional.

With this new config, Cargo exposes a new feature flag called faux. faux will only be built for tests and when the flag is enabled, which will be explained later.

Gating mocks to feature flag

Now that we have a faux feature flag, we want our mocks to be created when that flag is turned on. This is accomplished using the any attribute:

// mocks are available for both test and the faux feature flag
#[cfg_attr(any(test, feature = "faux"), faux::create)]
pub struct Renderer {
    /* snip */
    _inner: u8,
}

// mocks are available for both test and the faux feature flag
#[cfg_attr(any(test, feature = "faux"), faux::methods)]
impl Renderer {
    pub fn new() -> Renderer {
        /* snip */
        unimplemented!()
    }

    pub fn render(&mut self, texture: &Texture) -> Result<(), RenderError> {
        /* snip */
        unimplemented!()
    }
}

pub struct RenderError;
pub struct Texture;
fn main() {}

The key thing to remember here is replacing:

#[cfg_attr(test, ...)]
fn main()

with

#[cfg_attr(any(test, feature = "faux"), ...)]
fn main()

This tells Rust to use the faux attributes (create and methods) for either test or the faux feature flag. You can learn more about the any attribute in the Rust Reference.

These are all the changes necessary in our rendering library. The tests remain the same, and there are no implementation changes.

Using the faux feature

Let's now move on to a dependent of our rendering library. The dependency is marked in its Cargo.toml as:

[dependencies]
testable-renderer = * // some version

We would now like to use testable-renderer to render multiple textures for some World struct:

mod testable_renderer {
    extern crate faux;
    #[faux::create]
    pub struct Renderer {
        _inner: u8,
    }
    #[faux::methods]
    impl Renderer {
        pub fn new() -> Renderer {
            todo!()
        }
        pub fn render(&mut self, texture: &Texture) -> Result<(), RenderError> {
            todo!()
        }
    }
    pub struct Texture;
    impl Texture {
        pub fn render(&self, renderer: &mut Renderer) -> Result<(), RenderError> {
            renderer.render(self)
        }
    }
    #[derive(Debug)]
    pub struct RenderError;
}
use testable_renderer::{RenderError, Renderer, Texture};

struct World {
    player: Texture,
    enemy: Texture,
}

impl World {
    pub fn new() -> Self {
        World {
            player: Texture {},
            enemy: Texture {},
        }
    }

    pub fn render(&self, renderer: &mut Renderer) -> Result<(), RenderError> {
        self.player.render(renderer)?;
        self.enemy.render(renderer)?;

        Ok(())
    }
}
fn main() {}

We would like to write tests for our World::render method, but since rendering is an expensive opration, this is hard to do without mocks. Thankfully, testable-renderer is set up to expose its mocks, so we activate them by configuring the feature flag in our Cargo.toml:

[package]
# important so features turned on by dev-dependencies don't infect the
# binary when doing a normal build. This lets us have different feature
# flags in dev-dependencies vs normal dependencies.
resolver = "2"

[dependencies]
# our normal dependency does not activate `faux`, thus keeping it out of
# our released binary
testable-renderer = * # some version

[dev-dependencies]
# for tests, we activate the `faux` feature in our dependency so that
# we can use the exposed mocks
testable-renderer = { version = "*", features = ["faux"] }

# still depend on `faux` so we can use setup the mocks
faux = "^0.1"

The important takeaways are:

  • resolver = "2". This is needed so faux stays out of our normal builds. See the Cargo Reference.

  • Turn on the feature flag under [dev-dependencies]. We only want to have access to the mocks in testable-renderer when building tests.

We can now write tests as per usual:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn renders_the_world() {
        // the test target enables the faux feature on `testable-renderer`
        // thus allowing us to use the mocks of the *external* crate
        let mut renderer = Renderer::faux();
        faux::when!(renderer.render).then(|_| Ok(()));

        let world = World::new();
        world.render(&mut renderer).expect("failed to render the world")
    }
}

fn main() {}

Recap

The library that wants to export its mocks needs to:

  • Add faux as an optional dependency in Cargo.toml.

  • Create the mocks not only during tests, but also when the faux feature flag is turned on.

The library or application that wants to use the exported mocks needs to:

  • Change the feature resolver to "2" in its Cargo.toml. Be aware that if you are using a workspace, this needs to be changed in the workspace's Cargo.toml.

  • Add the dependency with the exported mocks under dev-dependencies with the faux flag enabled in Cargo.toml.

To see this in action, take a look at the example in the faux repository. testable-renderer is the library with the exported mocks and world-renderer is the application that uses these mocks.

🎉 🎉 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.

🔍 an inside look 🔍

faux is a traitless Rust mocking framework for creating mock objects out of user-defined structs. For more on faux's capabilities, take a look at the release blog post or the documentation.

faux creates mocks of your structs to be used in unit tests, making them fast and reliable.

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(&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(3)
    }
}

#[cfg(test)]
#[test]
fn service_does_the_right_thing() {
    let mut client = NetworkClient::faux();

    faux::when!(client.fetch).then(|i| {
        assert_eq!(i, 3, "expected service to send '3'");
        10
    });

    let subject = Service { client };
    let id = subject.do_stuff();
    assert_eq!(id, 10);
}

fn main() {
   let mut client = NetworkClient::faux();

   faux::when!(client.fetch).then(|i| {
        assert_eq!(i, 3, "expected service to send '3'");
        10
   });

   let subject = Service { client };
   let id = subject.do_stuff();
   assert_eq!(id, 10);
}

How does it work?

DISCLAIMER: this is a simplified version of how faux works as of February 2020, which may change in future versions. To see the most up to date transformations of your code, use cargo-expand

faux uses attributes to transform your structs into mockable versions of themselves at compile time.

The rest of the section focuses on code that looks like this:

#![allow(unused)]
fn main() {
pub struct NetworkClient {
    /* data here */
}

impl NetworkClient {
    pub fn new() -> Self {
        NetworkClient {
            /* data here */
        }
    }

    pub fn fetch(&self, a: u32) -> i32 {
        /* does some complicated stuff, maybe network calls */
        5
    }
}
}

faux, or any other mocking framework, needs to do two things to the code snippet above: create a fake version of NetworkClient, and provide a way to inject fake implementations of its methods.

Creating mockable structs

faux provides the attribute macro #[faux::create] to transform a struct definition into a mockable version of itself.

#![allow(unused)]
fn main() {
extern crate faux;
#[faux::create]
pub struct NetworkClient {
    /* data here */
}
}

From faux's perspective, a mockable version of a struct:

  • Is indistinguishable from the original struct, from a user's perspective
  • Can instantiate the original version because we do not always want a mocked instance
  • Can instantiate a mocked version without any additional data

At a high level, faux's transformation process consists of the following steps:

  1. Check that all struct fields are private; fail to compile otherwise
  2. Clones the definition of the struct
  3. Rename the original definition such that it is saved elsewhere
  4. Replace the cloned definition's fields with an enum of two variants, the fake and the real version
#![allow(unused)]
fn main() {
// same name so no one can tell the difference
pub struct NetworkClient(MaybeNetworkClient);

enum MaybeNetworkClient {
    // a fake does not need any data about the real network client
    Fake,
    // in case the user wants a real network client
    Real(RealNetworkClient)
}

// save the real definition somewhere else so it may still be created
struct RealNetworkClient {
    /* data here */
}

impl NetworkClient {
    // provide a method to create a fake instance of NetworkClient
    fn fake_please() -> NetworkClient {
        NetworkClient(MaybeNetworkClient::Fake)
    }
}
}

The code snippet above is a simplified version of the transformation #[faux::create] performs on NetworkClient. The mock requirements are satisfied:

  • Indistinguishable from the original struct
    • Although the transformed struct no longer has its original fields, callers that expect a NetworkClient continue to work as expected (provided they do not try to directly access those fields)
    • External information is kept the same (i.e., visibility, attributes)
  • Real instances can be created
    • The internal enum can be either a fake or a real instance
    • The real definition is kept in a struct with a different name for instantiation
  • Mock instances can be created
    • The fake variant of the internal enum knows nothing about RealNetworkClient

Creating mockable methods

faux provides the attribute macro #[faux::methods] to transform method definitions inside an impl block into mockable versions of themselves.

extern crate faux;
#[faux::create]
pub struct NetworkClient {}
#[faux::methods]
impl NetworkClient {
    pub fn new() -> Self {
        NetworkClient {
            /* data here */
        }
    }

    pub fn fetch(&self, a: u32) -> i32 {
        /* does some complicated stuff, maybe network calls */
        5
    }
}

fn main() {}

From faux's perspective, a mockable version of a method:

  • Is indistinguishable from the original method, from a user's perspective
  • Can call the real method because we do not always want a mocked method
  • Can run arbitrary code provided by the user

At a high level, faux's transformation process consists of the following steps:

  1. Clone the impl block
  2. Make the original impl block be an impl of the mocked struct instead
  3. Add when_* methods per public method in the cloned impl
  4. Modify the cloned methods to either proxy to or call the real instance
  5. Proxy the associated functions and private methods to the original definitions
pub struct NetworkClient(MaybeNetworkClient);
enum MaybeNetworkClient {
   Fake,
   Real(RealNetworkClient)
}
pub struct RealNetworkClient {}
// the numbers in the comments represent sections
// that will be explained in further detail later
impl NetworkClient {
    // (1)
    pub fn new() -> Self {
        Self(MaybeNetworkClient::Real(RealNetworkClient::new()))
    }

    // (2)
    pub fn fetch(&self, a: u32) -> i32 {
        // proxy to the real method for real instances
        // somehow get fake data when it is a mocked instance
        match self {
            Self(MaybeNetworkClient::Real(real)) => real.fetch(a),
            Self(MaybeNetworkClient::Fake) => {
                /* somehow get the fake data */
                10
            }
        }
    }
}

// (3)
mod real_impl_of_NetworkClient {
    // (3)
    type NetworkClient = super::RealNetworkClient;

    use super::*;

    impl NetworkClient {
        pub fn new() -> Self {
            NetworkClient {
                /* data here */
            }
        }

        pub fn fetch(&self, a: u32) -> i32 {
            /* does some complicated stuff, maybe network calls */
            5
        }
    }
}

fn main() {}

The code snippet above is a simplified version of the transformation #[faux::method] performs on the impl block. This is a bit more complicated than making a mockable struct and involves the following components:

  1. Returning a real instance

    Because we are only worried about mocking instances of methods, we can proxy to the real implementations of any associated function (a function that does not have a receiver, e.g., &self or self: Rc<Self>).

    However, because the new function above returns an instance of the mockable struct, while the real implementation returns an instance of the real struct, we need to to wrap the RealNetworkClient instance inside a NetworkClient.

  2. Methods

    Methods are fairly simple to handle. We match on the receiver, and then proxy to the real implementation if we are a real instance or somehow get the mock data if we are not. More on this somehow later.

  3. The real implementation

    Similar to the mockable struct case, we want to keep our real implementation somewhere so it can be called when needed. The hitch is that our real implementation refers to NetworkClient as if it were the real struct, e.g., when making a new instance, returning an object, or as the name in the impl statement. While we could go through the entire impl block and try to rename every mention of NetworkClient with RealNetworkClient, a lazier approach that works just fine is to use a type alias. However, type aliases are not yet allowed inside impl blocks. To get around this limitation, we put the alias and the real implementation in their own internal mod.

We have now satisfied the first two requirements of what constitutes a mockable method.

  • Is indistinguishable from the original method
    • By keeping the same function and method signatures, external callers cannot tell that the methods have been transformed.
  • Real methods can be called
    • The real implementation is saved so it can be called for real instances.

However, we have not satisfied the third requirement. There is no way for the user to provide arbitrary code to be run during tests.

Injecting mock methods

Ideally, we would like to have different mock instances of the same struct, each with their own mocked methods. This means that the mocked information belongs to the mocked instance. This changes our definition of our mockable NetworkClient from:

#![allow(unused)]
fn main() {
struct NetworkClient(MaybeNetworkClient);

enum MaybeNetworkClient {
    Fake,
    Real(RealNetworkClient),
}

pub struct RealNetworkClient { /* some data */ }
}

to:

#![allow(unused)]
fn main() {
struct NetworkClient(MaybeNetworkClient);

enum MaybeNetworkClient {
    Fake(MockStore),
    Real(RealNetworkClient),
}

pub struct RealNetworkClient { /* some data */ }

#[derive(Default)]
pub struct MockStore { /* store mocks somehow */ }

impl MockStore {
    pub fn get_mock(&self, name: &str) -> Option<Mock> {
        /* somehow return the mock matching the name */
        None
    }
}

pub struct Mock { /* represent a mock somehow */ }

impl Mock {
    pub fn call<I,O>(self, inputs: I) -> O {
        /* somehow produce an output */
        panic!()
    }
}
}

We have added a MockStore to the Fake variant of the MaybeNetworkClient enum. This allows us to store and retrieve mocks when we have a fake instance of NetworkClient. We derive Default for MockStore to denote that it can be created without any data. This is important because we need to be able to create a mock instance of the NetworkClient from nothing.

We can now now flesh out the mockable definition of fetch:

impl NetworkClient {
    pub fn fetch(&self, a: u32) -> i32 {
        match self {
            Self(MaybeNetworkClient::Real(real)) => real.fetch(a),
            Self(MaybeNetworkClient::Fake(mock_store)) => {
                mock_store
                    // retrieve the mock using the name of the function
                    .get_mock("fetch")
                    // check the mock was setup; panic if it was not
                    .expect("no mock found for method 'fetch'")
                    // pass in fetch's parameter to the mocked method
                    .call(a)
            }
        }
    }
}
pub struct NetworkClient(MaybeNetworkClient);
enum MaybeNetworkClient {
   Fake(MockStore),
   Real(RealNetworkClient)
}
pub struct RealNetworkClient {}
impl Mock {
    pub fn call<I,O>(self, inputs: I) -> O {
        panic!()
    }
}

pub struct MockStore {}
pub struct Mock {}

impl MockStore {
    fn get_mock(&self, name: &'static str) -> Option<Mock> {
        None
    }
}

impl RealNetworkClient {
    pub fn fetch(&self, a: u32) -> i32 {
        5
    }
}

fn main() {}

We are now just missing one key piece: saving mocks.

#![allow(unused)]
fn main() {
pub struct NetworkClient(MaybeNetworkClient);
enum MaybeNetworkClient {
   Fake(MockStore),
   Real(RealNetworkClient)
}
pub struct RealNetworkClient {}
pub struct MockStore {}

impl NetworkClient {
    pub fn when_fetch(&mut self) -> When<'_, u32, i32> {
        match &mut self.0 {
            MaybeNetworkClient::Fake(store) => When {
                store,
                method_name: "fetch",
                _marker: std::marker::PhantomData,
            },
            MaybeNetworkClient::Real(_) => panic!("cannot mock a real instance"),
        }
    }
}

// store the expected inputs and output in the type
struct When<'q, I, O> {
    method_name: &'static str,
    store: &'q mut MockStore,
    _marker: std::marker::PhantomData<(*const I, *const O)>,
}

impl<I, O> When<'_, I, O> {
    pub fn then(self, mock: impl FnMut(I) -> O) {
        self.store.save_mock(self.method_name, mock);
    }
}

impl MockStore {
    pub fn save_mock<I,O>(&mut self, name: &'static str, f: impl FnMut(I) -> O) {
        /* somehow save the mock with the given name */
    }
}
}

The When struct above provides a method to that saves the given mock inside the MockStore. We have also added a method to NetworkClient that returns an instance of When with information about the fetch method, thus allowing us to mock fetch.

We can now write code that looks like this:

fn main() {
let mut mock = NetworkClient::fake_please();
mock.when_fetch().then(|i| i as i32);
let fetched = mock.fetch(3);
assert_eq!(fetched, 3);
}
struct NetworkClient(MaybeNetworkClient);
enum MaybeNetworkClient {
    Fake(MockStore),
    Real(RealNetworkClient),
}
pub struct RealNetworkClient { /* some data */ }
#[derive(Default)]
pub struct MockStore { /* store mocks somehow */ }
impl MockStore {
    pub fn get_mock(&self, name: &str) -> Option<Mock> {
        None
    }
    pub fn save_mock<I,O>(&mut self, name: &'static str, f: impl FnMut(I) -> O) {
    }
}
pub struct Mock {}
impl Mock {
    pub fn call<I,O>(self, inputs: I) -> O {
        panic!()
    }
}
impl NetworkClient {
    pub fn fetch(&self, a: u32) -> i32 {
        match self {
            Self(MaybeNetworkClient::Real(real)) => real.fetch(a),
            Self(MaybeNetworkClient::Fake(mock_store)) => {
                mock_store
                    // retrieve the mock using the name of the function
                    .get_mock("fetch")
                    // check the mock was setup; panic if it was not
                    .expect("no mock found for method 'fetch'")
                    // pass in fetch's parameter to the mocked method
                    .call(a)
            }
        }
    }
    fn fake_please() -> NetworkClient {
         NetworkClient(MaybeNetworkClient::Fake(MockStore::default()))
    }
    pub fn when_fetch(&mut self) -> When<'_, u32, i32> {
        match &mut self.0 {
            MaybeNetworkClient::Fake(store) => When {
                store,
                method_name: "fetch",
                _marker: std::marker::PhantomData,
            },
            MaybeNetworkClient::Real(_) => panic!("cannot mock a real instance"),
        }
    }
}
struct When<'q, I, O> {
    method_name: &'static str,
    store: &'q mut MockStore,
    _marker: std::marker::PhantomData<(*const I, *const O)>,
}
impl<I, O> When<'_, I, O> {
    pub fn then(self, mock: impl FnMut(I) -> O) {
        self.store.save_mock(self.method_name, mock);
    }
}
impl RealNetworkClient {
    pub fn new() -> Self {
        RealNetworkClient {}
    }
    pub fn fetch(&self, a: u32) -> i32 {
        5
    }
}

You may have noticed that we largely omitted the implementation of MockStore and Mock. The implementations of these are pretty hairy, and thus out of scope for this blog post. However, feel free to read the source code of faux for more information. In reality, MockStore and Mock requires a few more bounds on the injected mock to both enable safe mocking and provide a version with more relaxed bounds that is gated by unsafe.

Final remarks

You have now seen a simplified version of the code faux produces. Remember that faux's expansions should be gated to only your test cfg, thus having no compile or run time impact on a cargo check or cargo build. If I missed anything, or if something was not clear, feel free to submit an issue or PR to faux as the blog also lives there as a GitHub page. I will do my best to clarify or to update the blog.

Feedback is always appreciated. Happy mocking!

🪂 landing v0.1 🪂

faux is a mocking library that allows you to mock the methods of structs for testing without complicating or polluting your code. This post is about the road to a beta version over the past year. To get started on faux, jump over to its repo and documentation!

Towards stability

The first release of faux came with this warning:

faux is in its early alpha stages, so there are no guarantees of API stability.

faux has stayed in this stage for over a year, releasing only in the 0.0.x range. This allowed faux to experiment and make breaking changes for the sake of of a better, more usable, API. However, part of the usability of a library is its stability. The new 0.1 release marks the beginning of a more stable API. Now, users can choose to only take non-breaking changes while faux still has the flexibility to experiment in a more controlled manner.

The old faux

A lot has changed over the past year for faux. This post focuses on:

  • Argument matchers
  • Mocking without closures
  • Safe interface

To demonstrate, here is some test code that uses faux from a year ago:

#[test]
fn bus_stops() {
    // creates a mock for bus::Client
    let mut bus_client = bus::Client::faux();

    let expected_stops = vec![bus::StopInfo {
        id: String::from("1_1234"),
        direction: String::from("N"),
        name: String::from("some bus"),
        lat: 34.3199,
        lon: 23.12005,
    }];

    // unsafe because mocks with references as inputs required them
    unsafe {
        when!(bus_client.stops).then(|q| {
            // manually assert that it was the expected input
            assert_eq!(
                *q,
                bus::StopsQuery {
                    lat: 34.32,
                    lon: 23.12,
                    lat_span: 0.002,
                    lon_span: 0.0005,
                    max_count: 20,
                }
            );
            // we are always returning the same data so a closure is overkill
            Ok(expected_stops.clone())
        })
    }

     /* snip */
    let area = Area {
        lat: 34.32,
        lon: 23.12,
        lat_span: 0.002,
        lon_span: 0.0005,
        limit: None,
    };

    let subject = Client::new(seattle_crime::Service::faux(), bus_client);

    let actual_stops = subject
        .bus_stops(&area)
        .expect("expected a succesful bus stop response");

    assert_eq!(actual_stops, expected_stops);
}
fn main() {}

Compared to faux today, there are three major issues with the test above:

  1. Even the simplest of mocks requires unsafe.
  2. Checking expected arguments is verbose.
  3. No shorthand to mock the return value without a closure.

The new faux

Now let's look at this same test today:

#[test]
fn bus_stops() {
    let mut bus_client = bus::Client::faux();

    let expected_stops = vec![bus::StopInfo {
        id: String::from("1_1234"),
        direction: String::from("N"),
        name: String::from("some bus"),
        lat: 34.3199,
        lon: 23.12005,
    }];

    // no more `unsafe` for mocking methods with references as arguments
    // when! supports argument matching
    when!(bus_client.stops(bus::StopsQuery {
        lat: 34.32,
        lon: 23.12,
        lat_span: 0.002,
        lon_span: 0.0005,
        max_count: 20,
    }))
    // for simple cases we can just mock the return value
    .then_return(Ok(expected_stops.clone()));

    /* snip */
    let area = Area {
        lat: 34.32,
        lon: 23.12,
        lat_span: 0.002,
        lon_span: 0.0005,
        limit: None,
    };

    let subject = Client::new(seattle_crime::Service::faux(), bus_client);

    let actual_stops = subject
        .bus_stops(&area)
        .expect("expected a succesful bus stop response");

    assert_eq!(actual_stops, expected_stops);
}
fn main() {}

The issues mentioned in the previous section have all been addressed:

  1. Mocking methods with references as arguments is no longer unsafe.
  2. when! now supports passing argument matchers.
  3. then_return! was added to mock just the return values for simple cases

For more information about the supported argument matchers, see the docs.

What's next

Guide

As the API surface of faux grows, it has become evident that a guide (WIP) is necessary to cover topics not appropriate for the API docs. I welcome suggestions on content that should be covered by the guide.

Call Verification

Speaking as a user of faux, my personal biggest feature request is call verification. In general, testing outputs is preferable to testing side effects, as the latter are more tied to implementation details. However, there are certain cases where you would want to verify a side effect, so faux should support this.

Existing issues

A lot of the features that exist in faux today came from people posting issues/PRs. Please feel free to look through the current issues and comment on any that would greatly help your testing experience if addressed.

New issues

If you have any feature requests that are not covered by existing issues, please submit a new issue.

Contributions

Over the past year, multiple contributors submitted issues and PRs to help improve and drive the direction of faux. A huge thanks to:

for the time you spent contributing to faux!

Continue the conversation in