Crate faux

source ·
Expand description

A library to create mocks out of structs.

faux allows you to mock the methods of structs for testing without complicating or polluting your code.

Part of faux’s philosophy is that only visible behavior should be mocked. In practice, this means faux only mocks public methods. Fields are not mocked, as they are data, not behavior. Private methods are not mocked, as they are invisible to others.

At a high level, faux is split into:

  • #[create]: transforms a struct into a mockable equivalent
  • #[methods]: transforms the methods in an impl block into their mockable equivalents
  • when!: initializes a method stub by returning a When. Passing optional argument matchers restricts which arguments will invoke the stub.
  • When: lets you stub a method’s return value or implementation

Getting Started

faux makes liberal use of unsafe Rust features, so it is only recommended for use inside tests. To prevent faux from leaking into your production code, set it as a dev-dependency in your Cargo.toml:

[dev-dependencies]
faux = "^0.1"

Examples

Simple

// restrict faux to tests by using `#[cfg_attr(test, ...)]`
// faux::create makes a struct mockable and generates an
// associated `faux()` function
// e.g.: `HttpClient::faux()` will create a mock `HttpClient` instance
#[cfg_attr(test, faux::create)]
pub struct HttpClient { /* */ }

// this is just a bag of data with no behavior
// so we do not attach `#[faux::create]`
#[derive(PartialEq, Clone, Debug)]
pub struct Headers {
    pub authorization: String,
}

// `faux::methods` makes every public method in the `impl` block mockable
#[cfg_attr(test, faux::methods)]
impl HttpClient {
    pub fn post(&self, path: &str, headers: &Headers) -> String {
        /* makes network calls that we'd rather not do in unit tests */
    }
}

#[cfg(test)]
#[test]
fn test() {
  // use the generated `faux()` function to create a mock instance
  let mut mock = HttpClient::faux();

  let headers = Headers { authorization: "Bearer foobar".to_string() };

  // use `faux::when!` to stub the behavior of your methods
  // you can specify arguments to match against when the stub is invoked
  faux::when!(
      // arguments are converted into argument matchers
      // the default argument matcher performs an equality check
      // use `_` to create a universal argument matcher
      // the argument matchers below specify to ignore the first argument
      // but that the second one must equal `headers`
      mock.post(_, headers.clone())
  )
  // stub the return value
  .then_return("{}".to_string());

  assert_eq!(mock.post("any/path/does/not/mater", &headers), "{}");
  assert_eq!(mock.post("as/i/said/does/not/matter", &headers), "{}");

  // if you want to stub all calls to a method, you can omit argument matchers
  faux::when!(mock.post).then_return("OK".to_string());
  let other_headers = Headers { authorization: "other-token".to_string() };
  assert_eq!(mock.post("other/path", &other_headers), "OK");
}

Stubbing the same method multiple times

A single method can be stubbed multiple times. When doing so, faux checks every stub for the method in a last-in-first-out fashion until it finds a stub whose argument matchers match the invocation arguments.

#[cfg(test)]
#[test]
fn test() {
  let mut mock = HttpClient::faux();
  let headers = Headers { authorization: "Bearer foobar".to_string() };
  let other_headers = Headers { authorization: "other-token".to_string() };

  // catch-all stub to return "OK"
  faux::when!(mock.post).then_return("OK".to_string());
  // stub for specific headers to return "{}"
  faux::when!(mock.post(_, headers.clone())).then_return("{}".to_string());

  assert_eq!(mock.post("some/path", &headers), "{}"); // matches specific stub
  assert_eq!(mock.post("some/path", &other_headers), "OK"); // matches catch-all stub
}

Stubbing implementation

faux supports stubbing of not just the return value but also the implementation of a method. This is done using then().

#[cfg(test)]
#[test]
fn test() {
  let mut mock = HttpClient::faux();
  let headers = Headers { authorization: "Bearer foobar".to_string() };

  faux::when!(mock.post).then(|(path, _)| path.to_string().to_uppercase());
  assert_eq!(mock.post("another/path", &headers), "ANOTHER/PATH");
}

Stubbing with non-static data

Let’s add a new method to our HttpClient that returns borrowed data. This cannot be stubbed using safe code, so faux provides .then_unchecked() and .then_unchecked_return() to stub such methods.

#[cfg_attr(test, faux::methods)]
impl HttpClient {
    pub fn host(&self) -> &str {
        /* returns a reference to some internal data */
    }
}

#[cfg(test)]
#[test]
fn test() {
  let mut mock = HttpClient::faux();

  // `then_unchecked()` and `then_unchecked_return()` require unsafe
  // they allow stubbing methods that return non-static values (e.g. references)
  // or to stub using non-static closures
  let ret = "some-value".to_string();
  unsafe { faux::when!(mock.host).then_unchecked_return(ret.as_str()) }
  assert_eq!(mock.host(), &ret);
}

Features

faux lets you stub the return value or implementation of:

  • Async methods
  • Trait methods
  • Generic struct methods
  • Methods with pointer self types (e.g., self: Rc<Self>)
  • Methods in external modules
  • Support for Debug, Default, Clone, Send, and Sync derive/auto traits.

faux also provides easy-to-use argument matchers.

Modules

  • Tools to check if an argument to a mocked method matches expectations.
  • Tools to stub the implementation or return value of your mocks.

Macros

  • Returns an ArgMatcher that succeeds if the provided closure returns true.
  • Returns an ArgMatcher that succeeds if the provided pattern matches.
  • Creates a When instance to stub a specific method in a struct.

Structs

  • Provides methods to stub the implementation or return value of the stubbed method.

Traits

  • Matcher for single argument of a method.

Attribute Macros

  • Transforms a struct into a mockable version of itself.
  • Transforms methods in an impl block into mockable versions of themselves.