2 releases

0.3.2 Jul 27, 2023
0.3.1 Jul 8, 2023
0.3.0 Jun 30, 2023
0.2.6 Jun 30, 2023
0.1.0 Jun 26, 2023

#166 in Testing

Download history 2/week @ 2024-02-14 9/week @ 2024-02-21 3/week @ 2024-02-28 2/week @ 2024-03-06 2/week @ 2024-03-13 6/week @ 2024-03-20 266/week @ 2024-03-27 1/week @ 2024-04-03

268 downloads per month

MIT/Apache

24KB
291 lines

stubby - stubbing that doesn't hurt

A tiny stubbing library that even your IDE can understand!

Why stub?

Stolen from Wikipedia:

In a unit test, mock objects can simulate the behavior of complex, real objects and are therefore useful when a real object is impractical or impossible to incorporate into a unit test. If an object has any of the following characteristics, it may be useful to use a mock object in its place:

  • the object supplies non-deterministic results (e.g. the current time or the current temperature);
  • it has states that are difficult to create or reproduce (e.g. a network error);
  • it is slow (e.g. a complete database, which would have to be prepared before the test);
  • it does not yet exist or may change behavior;
  • it would have to include information and methods exclusively for testing purposes (and not for its actual task).

Usage example

You can cargo add stubby, or in your Cargo.toml:

[dependencies]
stubby = "0.3"

Note that stubby has to go in your regular dependencies, not dev dependencies.

Now, some code that uses stubby:

use stubby::*;

struct TestStruct(StubbyState);

impl TestStruct {
    fn foo(&self) -> i32 {
        stub!(&self.0);
        10
    }
}

fn main() {
    let ts = TestStruct(StubbyState::default());
    assert_eq!(ts.foo(), 10);
}

#[test]
fn demo() {
    let mut mock = StubbyState::default();
    mock.insert(fn_name!(TestStruct::foo), 15);
    let ts = TestStruct(mock);
    assert_eq!(ts.foo(), 15);
}

Why is mocking/stubbing in Rust so difficult? (Comparison to mockall)

Mocking in Rust is difficult because strong typing and compiling to machine code don't give any flexibility to mess with data/behaviour, like you would have in a duck-typed language, or a language with some kind of interpreter.

Because of this, a common way for mocking libraries to work in Rust is by using a procedural macro to generate a new MockFoo from your impl Foo block, which has extra methods to allow you to customise return types and do fancy stuff. Then, at compile time, you either have #[cfg(not(test))] use lib::Foo or #[cfg(test)] use lib::MockFoo as Foo, which works because the generated MockFoo provides all the same methods the real Foo does. However, with both Rust Analyzer and the Rust plugin for Jetbrains IDEs, these compile-time conditional imports completely break auto-complete, type hinting, and sometimes even syntax highlighting. Plus, even where you're not using conditional imports, macro expansion for auto-complete/predictions often is less-well supported, especially with very complex generated types. This means that while your mock usage has zero overhead during runtime, it has a big overhead during the most important phase: develop-time.

How stubby works, and why it's more seamless

stubby is designed to avoid the pitfalls of conditional imports and procedural macros. It does this by instead storing mocking behaviour as an attribute of the struct you want to mock, instead of creating an entirely new struct. Avoiding procedural macros means slightly more boilerplate, though thanks to stub! and stub_if_some! that's usually only a single line per method. stubby still has zero cost when compiled in release mode by replacing its state with (), but it still presents the exact same interface in order to give your IDE the easiest time of it.

As a bonus, stubby compiles far faster as it has zero dependencies, only uses declarative macros, and has under 300 SLoC!

That having been said, it has only one of mockall's many features, and so if you're after a more feature-complete solution, check it out instead!

Type safety

Due to current limitations of the Rust compiler, stubbed types currently can't be type checked. However, if your project uses Rust nightly, you can opt into the type-safe feature, which removes the need for fn_name! and instead lets you directly pass in method signatures and have your types checked. Generate documentation locally with cargo doc to see the full updated interface/documentation

No runtime deps