la10736 / rstest

Fixture-based test framework for Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`#[fixture]` not returning the same instance when used by another fixture and a test?

rouge8 opened this issue · comments

I have a sample project using mockito with rstest and it looks like a fixture used by another fixture and a test are receiving different instances of the object (in this case, the server fixture is used by the my_struct fixture and test_it to get a URL, but the port differs between the two).

struct MyStruct {
    pub url: String,
}

#[cfg(test)]
mod tests {
    use rstest::{fixture, rstest};

    use super::*;

    #[fixture]
    async fn server() -> mockito::Server {
        mockito::Server::new_with_port_async(0).await
    }

    #[fixture]
    async fn my_struct(#[future] server: mockito::Server) -> MyStruct {
        return MyStruct {
            url: server.await.url(),
        };
    }

    #[rstest]
    #[tokio::test]
    async fn test_it(#[future] server: mockito::Server, #[future] my_struct: MyStruct) {
        let mut server = server.await;
        let my_struct = my_struct.await;

        // FAILS HERE
        assert_eq!(server.url(), my_struct.url);

        let _m = server
            .mock("GET", "/foo")
            .with_status(200)
            .with_body("foo bar stroke:#000 other:#000")
            .create_async()
            .await;

        let resp = reqwest::get(format!("{}/foo", my_struct.url))
            .await
            .unwrap();
        assert_eq!(resp.status(), 200);
    }
}

Is this expected behavior? Coming from pytest, I would have expected only a single mockito::Server to be created for test_it() that's used by both the test and the fixture.

Full example at https://github.com/rouge8/mockito-rstest

In order to have a static fixture you need to use #[once] annotation https://docs.rs/rstest/0.16.0/rstest/attr.fixture.html#once-fixture . Also in pytest you need to define a scope for your fixture to have this behavior https://docs.pytest.org/en/7.2.x/reference/fixtures.html#higher-scoped-fixtures-are-executed-first

But ....

There are some limitations when you use #[once] fixture. rstest forbid to use once fixture for:

  • async function
  • Generic function (both with generic types or use impl trait)

You can look at #141 to see how to work around this.... Rust is not python and the things can sometime be little bit more complicated.

I’m not talking about a static fixture. In pytest you get this behavior from the default function scope — a single test only creates one instance of the fixture, but with rstest I’m getting two instances.

Sorry.... I've missed your point. You're right that's the expected behavior (the default pytest behavior is the function scope).
Due the Rust's ownership model you cannot use the same instance in two places but just share reference...

Off course it is possible to implement something like this (I've already thought about it): a scoped once instance where you compute the object only if you don't already have one for the running asked scope, but that could not be the default behavior because in this case you cannot return the ownership of the fixture or a mutable reference to it but just a &'static of the fixture.

Ah right, I haven’t worked with Rust for a few months and didn’t think how ownership would make this hard…

@la10736 thanks for creating this crate!

I was also running into this just now.

Off course it is possible to implement something like this (I've already thought about it): a scoped once instance where you compute the object only if you don't already have one for the running asked scope, but that could not be the default behavior because in this case you cannot return the ownership of the fixture or a mutable reference to it but just a &'static of the fixture.

That sounds like a solution indeed.

(Sorry just browsing the repository and found this issue interesting)

Rust newbie here, but would forcing the fixture to return a Rc<T> solve the ownership issue? It could be automatically detected if possible, or a new attribute if not (say #[shared_fixture] for lack of a better name).

Fixtures are factory methods, and every time they are called generate a new instance. Of course the #[once] fixture is called once.

One way to fix this is have a single fixture that generates a test context which all tests use, and has the proper stuff

    struct TestContext{
        my_struct: MyStruct,
        server: mockito::Server,
    }

    #[fixture]
    async fn server() -> TestContext{
        let server = mockito::Server::new_with_port_async(0).await;
        let url = server.await.url();

        TestContext {
            server,
            my_struct: MyStruct {
                url 
            }
        }
    }