Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spy on mock service worker (msw)?

I'm starting to use the msw (mock service worker) after watching this example of how to use it for testing API calls in React applications.

Is there any way that we can spy on the mock service worker?

For example:

import React from 'react'
import { render, act, await } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { rest } from 'msw'
import { setupServer } from 'msw/node'

import SearchBox from '.'

const fakeServer = setupServer(
  rest.get(
    'https://api.flickr.com/services/rest/?method=flickr.photos.search',
    (req, res, ctx) => res(ctx.status(200), ctx.json({ data: { photos: { photo: [] },},}))
  )
)

beforeAll(() => {fakeServer.listen()})
afterEach(() => {fakeServer.resetHandlers()})
afterAll(() => fakeServer.close())

test('it calls Flickr REST request when submitting search term', async () => {
  const { getByLabelText } = render(<SearchBox />)
  const input = getByLabelText('Search Flickr')
  const submitButton = getByLabelText('Submit search')

  await act(async () => {
    await userEvent.type(input,'Finding Wally')
    await userEvent.click(submitButton)
  })

  await wait()

  // TODO: assert that the fakeServer was called once and with the correct URL
})

The component to test looks like this:

import React, { useState } from 'react'
import axios from 'axios'

import './index.css'

function SearchBox({ setPhotos }) {
  const [searchTerm, setSearchTerm] = useState('')

  const handleTyping = (event) => {
    event.preventDefault()
    setSearchTerm(event.currentTarget.value)
  }

  const handleSubmit = async (event) => {
    event.preventDefault()
    try {
      const restURL = `https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=${
        process.env.REACT_APP_API_KEY
      }&per_page=10&format=json&nojsoncallback=1'&text=${encodeURIComponent(
        searchTerm
      )}`
      const { data } = await axios.get(restURL)
      const fetchedPhotos = data.photos.photo
      setPhotos(fetchedPhotos)
    } catch (error) {
      console.error(error)
    }
  }

  return (
    <section style={styles.container}>
      <form action="" method="" style={styles.form}>
        <input
          aria-label="Search Flickr"
          style={styles.input}
          value={searchTerm}
          onChange={handleTyping}
        />
        <button
          aria-label="Submit search"
          style={styles.button}
          onClick={handleSubmit}
        >
          SEARCH
        </button>
      </form>
    </section>
  )
}

I have got a working test, but I feel it leans towards an implementation test since it uses a spy on the setPhotos

test('it calls Flickr REST request when submitting search term', async () => {
  const fakeSetPhotos = jest.fn(() => {})
  const { getByLabelText } = render(<SearchBox setPhotos={fakeSetPhotos} />)
  const input = getByLabelText('Search Flickr')
  const submitButton = getByLabelText('Submit search')

  await act(async () => {
    await userEvent.type(input, 'Finding Walley')
    await userEvent.click(submitButton)
  })

  await wait()

  expect(fakeSetPhotos).toHaveBeenCalledWith([1, 2, 3])
})
like image 606
Norfeldt Avatar asked Aug 14 '20 07:08

Norfeldt


2 Answers

The devs at mswjs are really nice and helpful. They took their time to advice me on how to approach it.

TLDR;

The current working test I got is fine - just suggested an alternative to jest.fn() - I do like the readability of the their suggestion:

test('...', async () => {
  let photos

  // Create an actual callback function
  function setPhotos(data) {
    // which does an action of propagating given data
    // to the `photos` variable.
    photos = data
  }

  // Pass that callback function as a value to the `setPhotos` prop
  const { getByLabelText } = render(<SearchBox setPhotos={setPhotos} />)

  // Perform actions:
  // click buttons, submit forms

  // Assert result
  expect(photos).toEqual([1, 2, 3])
})

Another thing I wanted to test was that it actually calls a valid REST URL.

You can reflect an invalid query parameter in the response resolver. If the query parameter is missing/invalid your real server would not produce the expected data, right? So with MSW your "real server" is your response resolver. Check the presence or value of that query parameter and raise an error in case that parameter is invalid.

rest.get('https://api.flickr.com/services/rest/?method=flickr.photos.search', 
     (req, res, ctx) => {   const method = req.url.searchParams.get('method')

  if (!method) {
    // Consider a missing `method` query parameter as a bad request.
    return res(ctx.status(400))   }

  // Depending on your logic, you can also check if the value of the `method`   // parameter equals to "flickr.photos.search".

  return res(ctx.json({ successful: 'response' })) })

Now, if your app misses the method query parameter in the request URL, it would get a 400 response, and shouldn't call the setPhotos callback in case of such unsuccessful response.

like image 137
Norfeldt Avatar answered Sep 30 '22 17:09

Norfeldt


If you want to avoid mocking you could spy on axios.get and assert that it was called correctly.

test('it calls Flickr REST request when submitting search term', async () => {
  const getSpy = jest.spyOn(axios, 'get');
  const { getByLabelText } = render(<SearchBox />)
  const input = getByLabelText('Search Flickr')
  const submitButton = getByLabelText('Submit search')

  await act(async () => {
    await userEvent.type(input,'Finding Wally')
    await userEvent.click(submitButton)
  })

  await wait()

  expect(getSpy).toHaveBeenCalledTimes(1)
})
like image 20
M A Avatar answered Sep 30 '22 16:09

M A