alexliesenfeld / httpmock

HTTP mocking library for Rust.
MIT License
435 stars 40 forks source link

Multiple Mocks isn't working as expected #60

Closed ttiimm closed 2 years ago

ttiimm commented 2 years ago

I'm trying to use the library to test some code that uses a Google API. The endpoint will return a pagination token in each response until there are no more resources to fetch. I was trying to test some code to see if the pagination was working and came across this unexpected behavior. In my test, I have two mocks with the same path, but different query string parameters -- here is a simplified version.

#[test]
fn test_different_qs_responses() -> Result<(), Box<dyn std::error::Error>> {
    let server = MockServer::start();

    let mock_first = server.mock(|when, then| {
        when.method(GET)
            .path("/v1/mediaItems")
            .query_param("pageSize", "3");
        then.status(200)
            .header("Content-Type", "application/json")
            .json_body(json!({
                "mediaItems": [],
                "nextPageToken": "the_next_page"
                }));
    });

    let mock_last = server.mock(|when, then| {
        when.method(GET)
            .path("/v1/mediaItems")
            .query_param("pageSize", "3")
            .query_param("pageToken", "the_next_page");
        then.status(200)
            .header("Content-Type", "application/json")
            .json_body(json!({
                "mediaItems": [],
                }));
    });

    let client = reqwest::blocking::Client::new();
    let mut query = vec![("pageSize", "3")];

    // first
    client.get(&server.url("/v1/mediaItems"))
          .query(&query)
          .send()?;

    query.push(("pageToken", "the_next_page"));

    // last
    client.get(&server.url("/v1/mediaItems"))
    .query(&query)
    .send()?;

    mock_first.assert();
    mock_last.assert();
    Ok(())
}

I'd expect mock_first to match the first request and mock_last to match the last since the query parameters specified in the match and in each request are different, but that doesn't appear to be the case. When I run the code I get this error:

thread 'test_different_qs_responses' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `1`: The number of matching requests was higher than expected (expected 1 but was 2)', C:\Users\likar\.cargo\registry\src\github.com-1ecc6299db9ec823\httpmock-0.6.5\src\api\mock.rs:207:13

Do I have a misunderstanding of how multiple mocks can be used or is this a bug in how mocks match requests?

alexliesenfeld commented 2 years ago

Thanks for creating this issue. There are two features in play here:

The solution is to change the first mock definition to not match if the pageToken query parameter is missing :

let mock_first = server.mock(|when, then| {
        when.method(GET)
            .path("/v1/mediaItems")
            .query_param("pageSize", "3")
            .matches(|req| {
                !req.query_params
                    .as_ref()
                    .unwrap()
                    .iter()
                    .any(|(k, _)| k.eq("pageToken"))
            });
        then.status(200)
            .header("Content-Type", "application/json")
            .json_body(json!({
            "mediaItems": [],
            "nextPageToken": "the_next_page"
            }));
    });

I consider using a matcher function a workaround though. It's planned to add dedicated matchers for the absence of values (e.g.,. query_param_missing("tokenPage"), as it was proposed in #44 . That would definitely help in your case.

Does this answer your question?

ttiimm commented 2 years ago

Yes, thanks for the detailed explanation, as well as the helpful library you're sharing. Aside from this issue, it's otherwise been really easy to use, so thanks for sharing this great work.

Having #44 could simplify the code, so look forward to it.

Have you considered modifying how requests are matched with mocks when there are multiple? For example, in my case since the second request had an extra query parameter one could say it more closely matches mock_last, than mock_first. Just wanted to see if you'd considered that as I can't imagine writing a test where you'd actually want to have two mocks that could potentially match a request.

Another way I've seen this done is with stateful behavior. So when the first request is made, the mock would transition to a new state and allow the second mock to be matched. Although that would be considerably more work and not sure as useful in this case considering the additional complexity.

Anyways -- appreciate the work and slick library. Thanks again.

alexliesenfeld commented 2 years ago

Yes, I have considered matching requests to the "closest" mock and it was a conscious decision to leave it out. In fact, httpmock already uses a weighted diff algorithm to find the closest request for a mock (used in the assert methods). The problem I've seen with matching a request to the "closest" mock is that it would sometimes not appear deterministic for the user and might not be easy to reason about what mock will kick in.

I have considered stateful behaviour and I think this functionality might be a nice addition to the library. However, time is the limiting factor here :-)