LukeMathWalker / wiremock-rs

HTTP mocking to test Rust applications.
Apache License 2.0
605 stars 70 forks source link

Named mocks #92

Open marlon-sousa opened 2 years ago

marlon-sousa commented 2 years ago

Hello,

Can you evaluate the following proposal?

If you agree, I can provide a PR.

Proposal

This issue aims to propose a named mock.

use case

Currently, mock expectations are verified once the wiremock server goes out of scope.

However, there are situations where we need to verify if side effects have happened before a wiremock server has the chance to go out of scope.

One (but not the only one) example is when a domain driven design test model is being used. For example, cucumber style tests need to verify if something has happened before the test ends, in a then function.

There are also situations where one wiremock server is used to support several tests running sequentially.

To support these use cases, we propose the creation of named mocks.

Named mock

A named mock shares several characteristics of the classical mock. It however:

side effects

This addition will not break usage for those who are already using this project, while enabling other use cases.

LukeMathWalker commented 2 years ago

Can you provide some code examples of what this would look like?

marlon-sousa commented 2 years ago

Sure.

This is pseldocode, although I have done my best to make it feel like rust.

It does not represent a extremely real use case, but it gets close. The main point here is that we might want to verify expectations on mocks when the "original" mock object is already gone and before the mock server goes out of scope.

Main reason for that is that parts of these processes might happen in different functions (like ucumber style enforces) or that you might want to make a more elaborate verification.

I have of course not provided an implementation, because I will do it only if you agree first with the proposal.

Rust is a great language for systems development, but it is also great for webservices development or anything else (almost). We just need to provide the developpers with a more convinient eco system and this crate for me leads RUST towards this goal. However, use cases like this one are not uncommon on webservices development or in other cases where integration tests are important, sometimes with greater prevalence than unit tests.

If you could accept something like this it would be tremendously helpful for many developers, myself surely considered here.

async fn create_mocks(mock_server: &mut MockServer) {
    // set a mock for handling user registration
    // we are interested here in catching registrations for any user
    #[derive(Deserialize, Serialize)]
    struct Schema {
        name: String,
    }    
    // create a named mock which will responde on post at /users
    let response = ResponseTemplate::new(200);    
    let mock = NamedMock::with_name("user_registration_mock")
        .given(method("POST"))
        .and(path("/users"))
        .and(body_json_schema::<Schema>)
        .respond_with(response.clone());
        mock_server.register_named(mock).await;
}// here the "original" mock goes out of scope. No panics, the server has owned it and is holding it

fn assert_registrations_are_successful(mock_server: &MockServer) {
    let named_mock = mock_server.report("user_registration_mock");
    assert!(named_mock.has_been_called_twice(), "expected two user registrations");
    // called_once, called_twice, called(n).times()
    // ok, two registrations happened. Are these the users we expected to have registered?
    // we will have to inspect our matched requests, because our mock could handle all user registrations, but we are interested in specific registrations
    #[derive(Deserialize, Serialize)]
    struct Schema {
        name: String,
    }    
    let requests = named_mock.matched_requests();
    let responses = requests.iter().map( | r | {
        let body = std::str::from_utf8(&r.body).unwrap();
        serde_json::from_str::<Schema>(&body).unwrap()
    }).collect::<Vec<Schema>>();
    assert!(responses.iter.any(| r | r.name == "user1"), "expected user 1 to be registered");
    assert!(responses.iter.any(| r | r.name == "user2"), "expected user 2 to be registered");
} // named mock go out of scope here, no panic

#[test]
async fn test_registration() {
    // create a mock server
    let mock_server = MockServer::start().await;
    // create mocks
    create_mocks().await;

    // register some users
    let bodys = [
        json!({"name": "user1"}),
        json!({"name": "user2"}),
    ];
    for body in bodys {
        let mut res = surf::post(format!("{}/users", &mock_server.uri()))
            .body_json(&body).unwrap().await.unwrap();
    }
    // now, we need to spy on behavior.
    // because we want to provide our own message and do not want to panic on stuff going out of scope, we will obtain the named mock and check what happened
    assert_registrations_are_successful(mock_server);
    // server will go out of scope here. Nothing will happen even if mock has failed on its expectations, we already checked that ourselves
} 
sazzer commented 1 year ago

A slightly simpler version of this, which would be fantastically useful, would be to just be able to get the number of calls to a mock that was itself given a name.

Something like:

    let mock_server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/hello"))
        .respond_with(ResponseTemplate::new(200))
        .named("Test Mock")
        .mount(&mock_server)
        .await;

Then the calls can be asserted later by doing:

    check!(mock_server.get_match_count("Test Mock") == 1);

Or possibly even something similar to received_requests() that takes the name and returns only requests that matched the mock with that name:

    check(mock_server.received_requests_for_mock("Test Mock").await.len() == 1);

Simpler still would be if the Request object returned by received_requests() just included the (optional) name of the mock that it matched, and then the caller can just use filter() to work out the correct ones:

    check!(mock_server.received_requests().await.unwrap().iter().filter(|req| req.name == "Test Mock").count() == 1);
LukeMathWalker commented 1 year ago

Thanks for pitching in @sazzer! I'd definitely be open to have either received_requests_for_named_mock or a get_match_count method. If you're willing to submit a PR, I'm happy to review it!

marlon-sousa commented 1 year ago

Hello @LukeMathWalker,

Could you clarify what received_requests_for_named_mock means exactly?

There are two proposals, mine and @sazzer 's. ARe you whiling to accept both?

@sazzer my main question here is that, for my scenarios to work, I need to be able to assert after mocks theirselves are out of scope, so that I can assert in latter steps of the flow.

I am not sure if your proposal covers that. If it does, then I think we might stick with yours.

sazzer commented 1 year ago

So, my desire was to use Wiremock as a fake remote API for integration tests. So it would be set up without expectations to always act as the remote service, and then have the ability afterwards to assert that calls either were made or were not made.

For my personal ask, whether the mocks have gone out of scope or not doesn't really matter. It was literally just "Given the name of a mock, how many times was it triggered" - which would work perfectly well whether they were out of scope or not.

marlon-sousa commented 1 year ago

I agree, this is also my issue, because the way it is, if mocks are out of scope before calls are made you have no way of asserting . If you do add expectations and mock is out of scope before calls are made, a panic will take place. I can try and code, something, unless you intend to do so. Just let me know.Obrigado,MarlonEm 28 de jan. de 2023, à(s) 10:55, Graham Cox @.***> escreveu: So, my desire was to use Wiremock as a fake remote API for integration tests. So it would be set up without expectations to always act as the remote service, and then have the ability afterwards to assert that calls either were made or were not made. For my personal ask, whether the mocks have gone out of scope or not doesn't really matter. It was literally just "Given the name of a mock, how many times was it triggered" - which would work perfectly well whether they were out of scope or not.

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: @.***>

sazzer commented 1 year ago

Go for it :) I've not had time to look at it yet!

LukeMathWalker commented 1 year ago

Simpler still would be if the Request object returned by received_requests() just included the (optional) name of the mock that it matched, and then the caller can just use filter() to work out the correct ones:

This is what I'd lean towards, but we can't modify the Request object because that'd be a breaking change. The same type is currently used as input for matchers, which in hindsight was probably a mistake.

For the sake of backwards compatibility, I'd suggest adding a new handled_requests() method on MockServer that returns an Option<Vec<ReceivedRequest>> type and marking received_requests() as deprecated. ReceivedRequest is a struct with private fields, one of which is the Request itself. It should also record if the request matched and, if the matched mock was named, the name of it. All this info should be accessible via getter methods.

What do you think?