Jest, developing and testing UI for Contact Us form

Lets create a simple Contact us form using TDD and Jest. Assuming we have developed some form components (or borrowed them from numerous component libraries available), we may turn our attention to the application logic (validation and communication with server). In other blog post I also consider server side implementation (using Express and MySQL). Source code could be found on Github.

Simple contact us form

Our simple form enabling users to send us messages along with their contact details. Here I paid little attention to the form design (which later could be tweaked through CSS styles anyway). Main focus is functionality: validation and communication with server. Our component looks as follows:

const App = () => {
  const counter = useRef(0)
  const [last, setLast] = useState(WAIT_MESSAGE)

  const nextRequest = async (request) => {
    const current = ++counter.current
    setLast(WAIT_MESSAGE)
    let message
    try {
      const res = await request()
      const data = (res.ok && await res.json()) || { message: `Error code ${res.status}: ${res.statusText}` }
      message = data.message
    } catch (e) {
      message = String(e)
    }

    if (current !== counter.current) {
      return
    }
    setLast(message)
  }

  useEffect(() => {
    nextRequest(() => fetch("http://localhost:8080/"))
    return () => counter.current = Infinity
  }, [])

  const onSubmit = async (value) => {
    await nextRequest(() => fetch("http://localhost:8080/contacts", {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(value)
    }))
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1>Contact Us</h1>
        <p>{last}</p>

        <Form onSubmit={onSubmit}>
          <Field label={"First name"} name="first_name" required>{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
          <Field label={"Last name"} name="last_name">{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
          <Field label={"Address"} name="address" >{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
          <Field label={"Phone"} name="phone">{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
          <Field label={"Email"} name="email" required email>{({ fieldProps }) => <TextField {...fieldProps} />}</Field>
          <Field label={"Message"} name="message" required>{({ fieldProps }) => <Textarea {...fieldProps} />}</Field>
          <input type="submit" value="Submit" />
        </Form>
      </header>
    </div>
  )
}

This code clarifies our requirements. Component sends requests to server to obtain server status and to save new user messages. POST and GET requests are sent to the server using fetch function. Status of the last communication with the server is stored and displayed using const [last, setLast] = useState(WAIT_MESSAGE) state variable. Stale replies are handled using counter (const counter = useRef(0)). Also asynchronous updates on unmounted component (giving warning messages in development) are avoided using useEffect clean up function (return () => counter.current = Infinity). Now when the scope of component functionality became clear, it is time to proceed to the actual development.

Jest: mocking fetch function

The component under consideration needs to deal with application logic (belongs to the Domain layer according to onion architecture terminology). So it is better to start development from BDD style user stories. Lets compile the list of the most important use cases. First of all, upon the visit to the page, users should see the last request status (both in case of success and failures). Secondly, the form should validate fields (displaying error messages). And, finally, form should send valid requests to the server.

As we do not want to depend on server implementation during development, we will want to stub fetch requests. Which is, luckily quite simple. For example, to mock successful server replies, I have created simple helper function:

const mockFetch = (response) => {
  const mockResponseData = response

  const mockJsonPromise = Promise.resolve(mockResponseData)
  const mockResponsePromise = Promise.resolve({
    ok: true,
    json: () => mockJsonPromise,
  })

  jest.spyOn(global, 'fetch').mockImplementation(() => mockResponsePromise)
}

A call to the fetch function returns Promise, resolving to a response with ok flag to test the success, and the number of asynchronous functions to fetch data. For this reason, the code above uses two promises. The mock itself is set using the jest spyOn function. As a fetch function is defined on a global object, we use it as a first argument to our mocking function - jest.spyOn(global, 'fetch').mockImplementation(() => mockResponsePromise). Mocking fetch failures is even simpler one-liner:

jest.spyOn(global, 'fetch').mockImplementation(() => { throw new Error("Some error") })

Using these helpers, writing, for example, first test scenarios is quite simple:

describe("user opens contact us", () => {
  it('API is active', async () => {
    await act(async () => {
      mockFetch({
        message: 'API Active'
      })

      const { getByText } = render(<App />)

      await waitForElement(() => getByText(/API Active/i))
      expect(global.fetch.mock.calls.length).toBe(1)
      global.fetch.mockRestore()
    })
  })

  it('API failure', async () => {
    await act(async () => {
      jest.spyOn(global, 'fetch').mockImplementation(() => { throw new Error("Some error") })
      const { getByText } = render(<App />)

      await waitForElement(() => getByText(/Error: Some error/i))
      expect(global.fetch.mock.calls.length).toBe(1)
      global.fetch.mockRestore()
    })
  })
})

Test cases begin from fetch mocking (original fetch implementation is restored at the end of ech test). A popular @testing-library/react library helps to avoid unnecessary tight coupling between tests and application, giving us ability quite flexibly reference sub-components. As App component is updated asynchronously, as a result of call to the server, tests are wrapped by await act(async () => {...}) functions. We wait for UI updates using await waitForElement(() => ...) wrappers.

Next

Form validation could be tested similarly. It is even simpler, as there is no need to mock fetch or wait for asynchronous UI updates. Just simulate user input using fireEvent.click and fireEvent.change testing library functions:

describe("form validation errors", () => {
  it('empty', () => {

    const { getByText, getAllByText } = render(<App />)

    fireEvent.click(getByText(/Submit/i))
    const errors = getAllByText(/required/i)
    expect(errors.length).toBe(3)
  })

  it('wrong email', () => {

    const { getByText, getAllByText, getByLabelText } = render(<App />)

    fireEvent.change(getByLabelText(/Email/i), { target: { value: 'email' } })
    fireEvent.click(getByText(/Submit/i))

    expect(getAllByText(/required/i).length).toBe(2)
    expect(getAllByText(/valid email/i).length).toBe(1)
  })
})

Request helper nextRequest and other request handling machinery do not really belong to the component and eventually should be moved (and tested) to a separate, more generic React Hook (which could be postponed until our application becomes bigger).

Now, when the main functionality is tested, one may wish to look at the server side implementation and test our form on real (development) server. To automate this process we may wish, eventually, to write integration tests.