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

Question: request handler request history #339

Closed
ilyaulyanov opened this issue Aug 16, 2020 · 14 comments
Closed

Question: request handler request history #339

ilyaulyanov opened this issue Aug 16, 2020 · 14 comments
Labels

Comments

@ilyaulyanov
Copy link

ilyaulyanov commented Aug 16, 2020

Is your feature request related to a problem? Please describe.
When using msw in a test environment, I'd like to be able to retrieve history of requests / responses for a certain handler. Do they get stored anywhere?
When using axios, there's a way to do it if you use axios-mock-adapter, that provides history per method.
Without mocking axios, I can't find a way to retrieve the last request made so I can verify that a request with certain params had been made.

Given

export const findPets = (petResponse?: Pet[]) =>
  rest.get(`${endpoint}/pets`, (req, res, ctx) => {
    const PetResponse = petResponse ?? PetStub.buildList(10)

    return res(ctx.delay(0), ctx.status(200), ctx.json(PetResponse))
  })

PetsList.test

  it('should return n results when limit is set', async () => {
    const FindPets = findPets()
    server.use(FindPets)

    render(<PetsList />) // Component makes a request with a query param {limit: 2}
    await waitFor(() => screen.getByText(/List of pets/i))

    /* Here I'd like to be able to retrieve GET request and assess it's params so I could validate that component's sending proper params, a la
	expect(FindPets.history[0]).toEqual(expect.objectContaining({ limit: 2})
    */
  })

Additional context

Name Version
msw 0.20.5
node 13
axios 0.19.2
@kettanaito
Copy link
Member

Hey, @ilyaulyanov. Thanks for reaching out.

I'm afraid accessing such list and performing assertions on it would be implementation details testing. Instead of checking internals of MSW and looking up if some handler was called with some parameters, think of it this way: if a handler is called with invalid parameters, your React component's output will be invalid. Assert what your component renders, not the network layer.

Take a look at the similar question that's been raised recently. I go into details and provide usage examples that lean your test towards testing what matters.

I can see such API being useful only for development purposes. It brings too much temptation to test if something was called, or was called with the right parameters, which is something MSW actively discourages. Test how your component reacts to the response, emulate unsuccessful and error responses, provide conditional mocked responses based on the request parameters in your response resolvers—this will give you much more confidence in your tests.

Hope this helps.

@ilyaulyanov
Copy link
Author

ilyaulyanov commented Aug 16, 2020

@kettanaito thanks for an elaborate and quick response. I agree that having the ability to see that would be tempting, and lead to implementation testing and all problems associated with it.

Having to write a conditional mocked response seems as the way to go here for me. Even though it has a risk of introducing false positive / negative results due to an incorrect implementation, it's still way more "real" than a mocked API module. I'm currently experimenting with msw and how it can help remove the "fake" layer of mocked modules and requests, when testing an API service and, maybe, use code generation to create handlers based on API schema.

It certainly feels like with msw, it's as close to 100% real as ever. Even though, the last bit of having to re-implement some parts of business logic (e.g. conditional response based on a value of a param) takes away from "realness" a bit.

If you see conditional responses as a solution to mine & similar issue, maybe it's worth adding it to the Recipes section? I can help if needed. The rest of the runbook, including recipes, was really helpful when setting it up for an existing app.

@kettanaito
Copy link
Member

kettanaito commented Aug 16, 2020

@ilyaulyanov consider reusing the condition logic from the rest of your app. Since mocks execute in the same context, you can call the same validation functions in the mocks, for example, that you do in the actual application's implementations. This is a solid way of ensuring you don't get false positive/negative results in your tests.

Even though, the last bit of having to re-implement some parts of business logic (e.g. conditional response based on a value of a param) takes away from "realness" a bit.

It may feel like repetition, but in reality it's depicting the behaviors that make your request valid. Not all business logic should be copied to response resolvers. The one that does helps you to reason about API changes over time, acting like a snapshot of an API contract once established. This is one of the arguments against any kind of sync between the actual server and mocks: it would destroy the reproducibility of a mock.

Thank you for the suggestion, I think it's a good idea to put these recommendations into a recipe. I've added the Request assertions recipe, please take a look.

@ilyaulyanov
Copy link
Author

@kettanaito

I've added the Request assertions recipe, please take a look.

Looks good to me!

@priley86
Copy link

hey @kettanaito - thanks for this explanation here, but I seem to have a caveat / related question for you...

How could one go about testing different UI state based on an Apollo query that utilizes a pollInterval?
https://www.apollographql.com/docs/react/data/queries/#options

I.e. first request responds with this result, second request delays, then responds...etc...

Any ideas?

@kettanaito
Copy link
Member

kettanaito commented Aug 28, 2020

Hey, @priley86. Thanks for reaching out.

The pollInterval on useQuery hook is a client-side option. It controls how your client polls for data. From the MSW perspective there's no polling, but a plain response to a request.

const GET_USER = gql`
  query GetUser {
    user: {
      firstName
    }
  }
`

function Example() {
  const { data } = useQuery(GET_USER, { pollInterval: 1000 }) // request each second

  // handle loading and error

  return <p>{data.firstName}</p>
}
// mocks.js
import { setupWorker, graphql } from 'msw'

const worker = setupWorker(
  graphql.query('GetUser', (req, res, ctx) => res(ctx.data({ user: { firstName: 'John' } }))
)

worker.start()

This way your useQuery hook will request data each 1000ms, while MSW request handler will respond to each of those requests as usual.

Does this answer your question? Feel free to clarify in case I got it wrong.

@priley86
Copy link

priley86 commented Aug 29, 2020

I think this example helps and helps me think differently about polling and testing w/ msw. Initially I was thinking that some kind of initial response could occur, and then a subsequent one after a delay with a different response (say to test the loading and error states above) using the same resolver. The client would be making the same query and just initiating a poll. However another potential way of testing these different loading/error states having the same query vars after a poll is just referencing different msw resolvers in your test. Thoughts?

@priley86
Copy link

i.e., no request history would be needed...

@kettanaito
Copy link
Member

kettanaito commented Aug 29, 2020

Initially I was thinking that some kind of initial response could occur, and then a subsequent one after a delay with a different response

However, this is not how polling works. Polling is entirely client-side technique that provides data synchronization via repetitive request in a defined time interval. From the server's perspective polling doesn't exist, as the server receives a request and responds to it as usual. In a traditional HTTP communication server cannot send a response without a preceding request first. You may be confusing polling with real-time subscriptions (i.e. WebSocket), which are not yet supported by MSW (see #156).

However another potential way of testing these different loading/error states having the same query vars after a poll is just referencing different msw resolvers in your test.

Sorry, I'm not sure I follow you on this one. Please, could you share some code (pseudo-code is fine) of how you see this working?

@NMinhNguyen
Copy link

Found this issue by searching for negative and I was wondering if there is a recommended pattern for checking that a request was not made? For example, you might want to test that you correctly specified useEffect dependency array to avoid fetching on every render:

useEffect(() => {
  fetch(`/api/todo/${todo.id}`)
}, [todo])

So far the only way I can do think of is spying on fetch and asserting expect(fetch).not.toHaveBeenCalled() but not sure if there is a better way.

@kettanaito
Copy link
Member

kettanaito commented Feb 26, 2021

@NMinhNguyen can you assert the opposite, that only known requests were made?

You can use the onUnhandledRequest option for that:

// Fail the test if it makes a request not listed in the handlers.
server.listen({ onUnhandledRequest: 'error' })

If you wish to scope the list of allowed requests, use the .resetHandlers() method and provide the next handlers to it:

// The server may be created for all tests and may include handlers
// that describe requests you wish not to be made in this particular case.
const server = setupServer(...handlers)

server.listen({ onUnhandledRequest: 'error' })

afterEach(() => {
  // Reset handlers after each test to the list of the "handlers"
  // initially passed to "setupServer" call.
  server.resetHandlers()
})

it('does not make any unknown requests', () => {
  // Only the handlers listed below will be treated as known.
  server.resetHandlers(rest.get('/api/todo/:id', resolver)

  // ...run your code
  // ...assert result
})

The suggestion above is applicable only if your initial handlers describe requests that you don't wish to be made in the currently tested component.

@NMinhNguyen
Copy link

NMinhNguyen commented Feb 26, 2021

Thanks @kettanaito, is it possible to make tests fail onUnhandledRequest though? Because I think it just warns?

@NMinhNguyen can you assert the opposite, that only known requests were made?

In my case, I actually don't want any requests to be made (due to some conditional logic). Is there a valid way to call resetHandlers to support this use case? Would you perhaps reset it to a ctx.status(500) handler or something? Mostly so that if a request is made, it gets rejected by the backend.

Regarding resetHandlers with a parameter, the docs led me to believe that it replaces the initial handlers entirely: https://mswjs.io/docs/api/setup-server/reset-handlers#replacing-initial-handlers, I didn't know that server.resetHandlers() would help you recover from that.

@kettanaito
Copy link
Member

You can use the onUnhandledRequest: 'error' to produce an error, but neither option would fail a test.

I didn't know that server.resetHandlers() would help you recover from that.

That's how I'd expect it to work, but I haven't tried to use it this way.

In my case, I actually don't want any requests to be made

I don't think we have a suitable API to recommend in this case at the moment. Based on the requirements, what you'd probably need is to capture all requests and make them return 500 responses/throw an error.

@NMinhNguyen
Copy link

That's how I'd expect it to work, but I haven't tried to use it this way.

You're right, I just checked the implementation, and I'm pretty sure it works the way you described 🙂

I don't think we have a suitable API to recommend in this case at the moment. Based on the requirements, what you'd probably need is to capture all requests and make them return 500 responses/throw an error.

Makes sense, thank you!

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

No branches or pull requests

4 participants