How to run setup code before any tests run in Rust?
Asked Answered
T

3

64

I have a Rust app (a simple interpreter) that needs some setup (initialize a repo) before the environment is usable.

I understand that Rust runs its tests (via cargo test) in a multithreaded manner, so I need to initialize the repo before any tests run. I also need to do this only once per run, not before each test.

In Java's JUnit this would be done with a @BeforeClass (or @BeforeAll in JUnit 5) method. How can I acheive the same thing in Rust?

Twentieth answered 19/9, 2019 at 7:39 Comment(0)
P
60

There's nothing built-in that would do this but this should help (you will need to call initialize() in the beginning of every test):

use std::sync::Once;

static INIT: Once = Once::new();

pub fn initialize() {
    INIT.call_once(|| {
        // initialization code here
    });
}
Pernicious answered 19/9, 2019 at 7:54 Comment(4)
I have error when using this code. Error: "static INIT: Once = Once::new(); = note: #[warn(dead_code)] on by default". Please help me to fix it. Thanks!Anasarca
@TheSun you need to call it before every test as Jussi suggestedKetosis
@Ketosis thank you, I solved my problem by using lazy_staticAnasarca
more explanation or links would be usefulBirth
C
56

If you use the ctor crate, you can take advantage of a global constructor function that will run before any of your tests are run.

Here's an example initialising the popular env_logger crate (assuming you have added ctor to your [dev-dependencies] section in your Cargo.toml file):

#[cfg(test)]
#[ctor::ctor]
fn init() {
    env_logger::init();
}

The function name is unimportant and you may name it anything.

Colangelo answered 16/8, 2020 at 21:29 Comment(2)
I should put this into each .rs file?Metametabel
@Metametabel Every test file is a different executable program, so yes. But you could also create a common module so that you only need to define it onceCuthburt
N
27

Just to give people more ideas (for example, how not to call setup in every test), one additional thing you could do is to write a helper like this:

fn run_test<T>(test: T) -> ()
    where T: FnOnce() -> () + panic::UnwindSafe
{
    setup();    
    let result = panic::catch_unwind(|| {
        test()
    });    
    teardown();    
    assert!(result.is_ok())
}

Then, in your own tests you would use it like this:

#[test]
fn test() {
    run_test(|| {
        let ret_value = function_under_test();
        assert!(ret_value);
    })
}

You can read more about UnwindSafe trait and catch_unwind here: https://doc.rust-lang.org/std/panic/fn.catch_unwind.html

I've found the original idea of this test helper in this medium article by Eric Opines.

Also, there is rstest crate which has pytest-like fixtures which you can use as a setup code (combined with the Jussi Kukkonen's answer:

use std::sync::Once; 
use rstest::rstest;
static INIT: Once = Once::new();

pub fn setup() -> () { 
    INIT.call_once(|| {
        // initialization code here
    });
}

#[rstest]
fn should_success(setup: ()) {
    // do your test
}

Maybe one day rstest will gain scopes support and Once won't be needed anymore.

Nucellus answered 19/9, 2019 at 11:28 Comment(2)
Nice tech work, but too much barebones for running a test..Ketosis
rstest does have the #[once] attribute macro for #[fixture] functions - but I think it still uses statics under the covers (i.e. no/unreliable drop)Spermine

© 2022 - 2024 — McMap. All rights reserved.