Skip to main content

mock-inspect: Mock network requests and observe how these requests were made

This article is part of a series about the open-source Node.js testing library mock-inspect of which I am the main contributor and author. Check out the entire series here!

mock-inspect is an open-source library for Node.js which allows you to mock network requests in your tests and to inspect how these requests have been made in the application. Has the endpoint been called? Which request body and request headers did the application pass along? This knowledge can be used to create your very own assertions, entirely depending on your use case.

Since mock-inspect is a Node.js library, it can be used both for frontend as well as backend applications. In this blog post, we want to explore the use case in a frontend project using React and how we can elevate our tests thanks to mock-inspect.

Application Outline

To demonstrate the usage of mock-inspect, I have prepared an example frontend application which is inspired by a real-life use case, the tray.io marketing website. Users can fill out a form to request a specific type of demo for the product. When the form is submitted, an API endpoint is pinged with all the properties that the user supplied. The frontend application always calls the same endpoint, no matter which selection the user makes. That endpoint will return a 200 status code, no matter what. Once the frontend application has forwarded the properties, it displays a message which says that the user is going to receive an e-mail. Note that the e-mail is scheduled to be sent; it could also be sent a few hours later.

A recording of the user flow:

Below is the React code for this simple application (feel free to skip over this one):

import "./App.css"

import {useState} from "react"

function App() {
  const [name, setName] = useState("")
  const [email, setEmail] = useState("")
  const [demoType, setDemoType] = useState("")
  const [submitted, setSubmitted] = useState(false)

  const nameHandler = (event) => {
    setName(event.target.value)
  }

  const emailHandler = (event) => {
    setEmail(event.target.value)
  }

  const radioButtonHandler = (event) => {
    setDemoType(event.type)
  }

  const forwardInputs = async () => {
    const dataToSend = {name, email, demoType}
    const response = await fetch("https://someapi.com/request-demo", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(dataToSend),
    })
    if (response.status === 200) {
      setSubmitted(true)
    }
  }

  const getDemoForm = () => {
    return (
      <div className="App-header">
        <input type="text" placeholder="Your name" onChange={nameHandler} />
        <input type="text" placeholder="Your e-mail" onChange={emailHandler} />
        <label htmlFor="live">
          Live Demo
          <input
            type="radio"
            id="live"
            value="live"
            onChange={radioButtonHandler}
          />
        </label>
        <label htmlFor="recorded">
          Recorded Demo
          <input
            type="radio"
            id="recorded"
            value="recorded"
            onChange={radioButtonHandler}
          />
        </label>
        <label htmlFor="1-2-1">
          1-2-1 with a representative
          <input
            type="radio"
            id="1-2-1"
            value="1-2-1"
            onChange={radioButtonHandler}
          />
        </label>
        <br />
        <input type="button" value="Submit" onClick={forwardInputs} />
      </div>
    )
  }

  const getThankYouPage = () => {
    return (
      <div className="App-header">
        Thank you! We will send you a confirmation e-mail.
      </div>
    )
  }

  return (
    <div className="App">{!submitted ? getDemoForm() : getThankYouPage()}</div>
  )
}

export default App

How do we test this? In order to make our unit tests stable, we will want to mock the network request so that the tests work independently from the real-world application. Many mocking tools will advise that a good test should check if the application correctly consumes the API response. But as we have seen in the use case description, there is nothing for our frontend application to consume - the API will return a 200 status code no matter what! In essence, all our application does is forwarding some properties. This is precisely why designing our test around the consumption of the API response would be too simple; we would not test enough by merely testing whether the application reacts to the 200 status code. What is actually important about our use case is that the frontend forwards the correct properties and in the correct format.

Let’s start writing a test for our application. As a start, we are going to use the approach of checking that the application consumes the API response. Below is a test for this in which we are already using mock-inspect to set up the request mock. Apart from that, we are using React Testing Library to enter text into the inputs and to select our desired demo type.

import {render, screen, fireEvent, waitFor} from "@testing-library/react"
import App from "./App"
import {mockRequest} from "mock-inspect"

test("shows Thank You page on submitting the form", async () => {
  mockRequest({
    requestPattern: "https://someapi.com/request-demo",
    requestMethod: "POST",
    responseStatus: 200,
  })

  render(<App />)
  const nameInput = screen.getByPlaceholderText("Your name")
  fireEvent.change(nameInput, {target: {value: "Han Solo"}})
  const emailInput = screen.getByPlaceholderText("Your e-mail")
  fireEvent.change(emailInput, {target: {value: "i-shot-first@coruscant.sw"}})
  const recordedRadio = screen.getByLabelText("Recorded Demo")
  fireEvent.click(recordedRadio)
  const submitButton = screen.getByDisplayValue("Submit")
  expect(submitButton).toBeInTheDocument()
  fireEvent.click(submitButton)
  await waitFor(() => {
    expect(submitButton).not.toBeInTheDocument()
    const thankYouPage = screen.getByText(
      "Thank you! We will send you a confirmation e-mail.",
    )
    expect(thankYouPage).toBeInTheDocument()
  })
})

Great, this test passes! Job done, right? After all, our application is forwarding the request payload of {"demoType": "recorded", "name:" "Han Solo", "email": "i-shot-first@coruscant.sw"} to the API. Well, … it doesn’t. Our test let a bug slip: instead of forwarding "demoType": "recorded", it sends "demoType": "change". Why is that? This is caused by a problem with the handler function of the radio buttons. We made a human error and sent event.type instead of event.target.value, probably because we were too focused on the word “type” in the setDemoType function. But we can fix this:

    // Inside App.js
    const radioButtonHandler = (event) => {
-       setDemoType(event.type)
+       setDemoType(event.target.value)
    }

But our test still doesn’t confirm whether the right properties are forwarded. Yes, we land on the Thank You page if we get a 200 status code from the backend; but we don’t know whether we send the right things. This is where mock-inspect comes to the rescue.

Whenever you mock a network request using mock-inspect’s mockRequest method, you receive a class instance. This instance contains a few useful methods, such as the method expectRequestToHaveBeenMade() which would throw an error if the application hasn’t made the request. Another useful method is inspect() which will return the request payload and the request headers which have been used by the application when it made the request. Should the request not have been made at the time of calling inspect(), the method would throw an error (as it internally calls expectRequestToHaveBeenMade()).

Thanks to the inspect() method, we can complete our test and assert that the frontend application does indeed forward the correct properties. Now we really can be sure everything works as expected!

test("shows Thank You page on submitting the form", async () => {
- mockRequest({
+ const demoRequest = mockRequest({
    requestPattern: "https://someapi.com/request-demo",
    requestMethod: "POST",
    responseStatus: 200,
  })

  render(<App />)
  const nameInput = screen.getByPlaceholderText("Your name")
  fireEvent.change(nameInput, {target: {value: "Han Solo"}})
  const emailInput = screen.getByPlaceholderText("Your e-mail")
  fireEvent.change(emailInput, {target: {value: "i-shot-first@coruscant.sw"}})
  const recordedRadio = screen.getByLabelText("Recorded Demo")
  fireEvent.click(recordedRadio)
  const submitButton = screen.getByDisplayValue("Submit")
  expect(submitButton).toBeInTheDocument()
  fireEvent.click(submitButton)
  await waitFor(() => {
    expect(submitButton).not.toBeInTheDocument()
    const thankYouPage = screen.getByText(
      "Thank you! We will send you a confirmation e-mail.",
    )
    expect(thankYouPage).toBeInTheDocument()
  })

+ // is also called inside `inspect()` - here for clarity
+ demoRequest.expectRequestToHaveBeenMade()
+ const {requestBody} = demoRequest.inspect()
+ expect(requestBody).toEqual({
+   name: "Han Solo",
+   email: "i-shot-first@coruscant.sw",
+   demoType: "recorded",
+ })
})

Conclusion

We have seen how mock-inspect can help you write better and more meaningful tests. If you want to learn more about the library, head over to our GitHub page where you can find detailed instructions on how to use mock-inspect. There, we also outline other functionalities we haven’t discussed yet, such as how to handle GraphQL and how to auto-generate responses using a GraphQL schema!

Keep an eye out for other upcoming articles in this series. In future blog posts, we will look at how mock-inspect works behind the scenes and how it can be used to facilitate contract testing.


Chameleon graphic by Анна Куликова from Pixabay