---
title: Don't let failures spread over your suite with process-based tests isolation
date: 2020-12-28
series: rust-testing-tricks
tags: [rust, testing]
---

Being able to precisely control what failures in underlying systems occur and at what time can be really useful in achieving a fast and stable test suite. While I am a big proponent of dependency inversion and being able to control dependencies via the explicit injection points in your API, sometimes it's impractical to do so. This is where [`fail`](https://crates.io/crates/fail) can help us immensely, providing an escape hatch for situations like those as it allows to inject failures into previously defined failure points.

It comes at a price though. If you would mix your other unit tests and tests activating fail points you will notice some unexpected failures in the test suite. As `cargo test` runs tests in parallel by default, the tests activating a fail point can interfere with another test that did not want that fail point active at all that is ran at the same time. The crate authors [recommend](https://docs.rs/fail/#usage-in-tests) running all of the tests using fail points in a separate executable and using `FailScenario` to serialise test execution.

There is another way, that I found simpler for the way I write tests, if you allow for yet another helper crate. We can run each test in a separate process, effectively isolating it from the rest, stopping failures from spreading.

Let's take a look at an example from [`bakare`](https://git.sr.ht/~cyplo/bakare) - my experiment in writing a backup system.

`cargo.toml`

```toml
[dependencies]
fail = "0.4"

[dev-dependencies]
two-rusty-forks = "0.4.0"

[features]
failpoints = [ "fail/failpoints" ]
```

`lock.rs`

```rust
/// this function is called from `Lock::lock()`
fn create_lock_file(lock_id: Uuid, index_directory: &VfsPath) -> Result<()> {
    ...
    fail_point!("create-lock-file", |e: Option<String>| Err(anyhow!(e.unwrap())));
    let mut file = lock_file_path.create_file()?;
    ...
}

mod must {
    use super::Lock;
    use anyhow::Result;

    /// only import the macro when `failpoints` feature is enabled
    #[cfg(feature = "failpoints")]
    use two_rusty_forks::rusty_fork_test;
    use vfs::{MemoryFS, VfsPath};

    #[test]
    /// this is a normal unit test
    /// we don't want for it to be affected by the fail points being active
    fn be_released_when_dropped() -> Result<()> {
        let temp_dir: VfsPath = MemoryFS::new().into();
        {
            let _lock = Lock::lock(&temp_dir);
        }
        let entries = temp_dir.read_dir()?.count();

        assert_eq!(entries, 0);
        Ok(())
    }

    #[cfg(feature = "failpoints")]
    rusty_fork_test! { /// use the macro to create a separate process for this test
        #[test]
        fn be_able_to_lock_when_creating_lock_file_fails_sometimes() {
            /// activate the fail point
            fail::cfg("create-lock-file", "90%10*return(some lock file creation error)->off")
                .unwrap();
            let path = MemoryFS::new().into();

            let lock = Lock::lock(&path).unwrap();
            lock.release().unwrap();
        }
    }

    ...
}

```