Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple Mocks isn't working as expected #60

Closed
ttiimm opened this issue Jan 6, 2022 · 3 comments
Closed

Multiple Mocks isn't working as expected #60

ttiimm opened this issue Jan 6, 2022 · 3 comments

Comments

@ttiimm
Copy link

ttiimm commented Jan 6, 2022

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
Copy link
Owner

alexliesenfeld commented Jan 6, 2022

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

  • In your example the HTTP request matches both mocks. This is because the when part defines all attributes a mock requires to be triggered. In this case, the requirements of mock_first are a subset of mock_second. So if you send a request that matches mock_second, then the request will also match mock_first.
  • The second effect is that mocks are matched in the order they were defined. If the mock server finds a mock that matches the current HTTP request, it will send a response and consider the request done. This is why first_mock has call count 2 and mock_second a call count of 0.

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
Copy link
Author

ttiimm commented Jan 7, 2022

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.

@ttiimm ttiimm closed this as completed Jan 7, 2022
@alexliesenfeld
Copy link
Owner

alexliesenfeld commented Jan 7, 2022

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 :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants