What is a good way of cleaning up after a unit test in Rust?
Asked Answered
K

2

26

Since the test-function aborts on a failure, one cannot simply clean up at the end of the function under test.

From testing frameworks in other languages, there's usually a way to setup a callback that handles cleanup at the end of each test-function.

Karyosome answered 7/7, 2016 at 18:58 Comment(0)
W
25

Since the test-function aborts on a failure, one cannot simply clean up at the end of the function under test.

Use RAII and implement Drop. It removes the need to call anything:

struct Noisy;

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("I'm melting! Meeeelllllttttinnnng!");
    }
}

#[test]
fn always_fails() {
    let my_setup = Noisy;
    assert!(false, "or else...!");
}
running 1 test
test always_fails ... FAILED

failures:

---- always_fails stdout ----
    thread 'always_fails' panicked at 'or else...!', main.rs:12
note: Run with `RUST_BACKTRACE=1` for a backtrace.
I'm melting! Meeeelllllttttinnnng!


failures:
    always_fails

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured
Wolter answered 7/7, 2016 at 20:11 Comment(6)
Is there a way to do this without polluting the source? Are there any frameworks that are good for this?Spout
@VladyVeselinov You declare the struct and impl within the test function.Polyamide
@VladyVeselinov have a look at crates.io/search?q=scope%20guard and pick one that suits your needs.Gam
How could this be applied for async calls?Dover
@TamilVendhanKanagarasu unfortunately, "async drop" is a complicated matter. In some cases, you can spin up a lightweight reactor (like one from the futures crate) to perform the async operation.Wolter
May I make the obvious remark that the elegance of this is that it leverages Rust's own most basic scoping rules to do an essential task specific for testing for which other languages might indeed need a framework?Rosaceous
R
1

Just to elaborate on the mighty Shepmaster's brilliant solution, and partly in answer to the comment by Vlady Veselinov that this in some way means introducing "superfluous" code into your test, I offer this idea of a "general purpose" Noisy where you stipulate only the cleanup closure you need:

struct Noisy<'a> {
    closure: &'a dyn Fn() -> (),
}

impl Noisy<'_> {
    fn new(closure: &dyn Fn() -> ()) -> Noisy {
        Noisy { closure }
    }
}

impl Drop for Noisy<'_> {
    fn drop(&mut self) {
        let _ = &(self.closure)();
    }
}

// the idea with this test is that some files are created in a temporary dir
// and must be deleted regardless of outcome at the end of the test...
#[test]
fn index_gather_documents_does_that() -> Result<()> {
    let dir = env::temp_dir();
    let delivered_temp_dir_path = dir.as_path();
    let temp_rust_testing_dir_path = delivered_temp_dir_path.join("rust_testing");
    let cleanup_dir_path = temp_rust_testing_dir_path.clone();
    let cleanup_dir_path_str = cleanup_dir_path
        .to_str()
        .ok_or_else(|| anyhow!("str failure! |{:?}|", cleanup_dir_path))?;

    let teardown_binding = || {
        std::fs::remove_dir_all(cleanup_dir_path_str);
    };
    let _noisy = Noisy::new(&teardown_binding);
    // NB "_" here will not work: drop() will happen immediately

    // ... test proper: start creating the files
}

I am indebted to cafce25 for what looks like a more elegant solution:

use std::fmt::Display;
use std::path::PathBuf;

pub struct Noisy<F: FnMut()> {
    closure: F,
}

impl<F: FnMut()> Noisy<F> {
    pub fn new(closure: F) -> Self {
        Noisy {
            closure,
        }
    }
}

impl<F: FnMut()> Drop for Noisy<F> {
    fn drop(&mut self) {
        (self.closure)()
    }
}

#[test]
fn my_test() {
    let cleanup_dir_path = PathBuf::from("src");
    let _noisy = Noisy::new(|| {
        std::fs::remove_dir_all(&cleanup_dir_path).unwrap()
    });
    ...
}

... unwrap() in the closure causes the test to fail in case of error (as it should).

Rosaceous answered 2/3 at 19:21 Comment(3)
Just a couple of hints to_str() for a path is really bad form because no OS I know of enforces utf-8 encoded file names, Noisy should also probably be Noisy<F>(F) so it does not need to store a pointer, but can take a closure or fn item directly.Erinnerinna
Thanks, I understand the first point but not the second. Care to suggest an edit? Although I'm going to try to understand the point ...Rosaceous
Something like thisErinnerinna

© 2022 - 2024 — McMap. All rights reserved.