Write test for React: component, hoc, hooks, promise, saga, graphql, custom hooks

09 October, 2020

Read this article on medium

In this article, I will talk about writing test for react component, connected component, hoc, react hooks, redux saga function (generator function), and function with promise. In the end, I will also talk about an important technique: how to mock functions.

Examples are available in this git repo: react test demos

1.Test for react component

We will begin with tests for react component with Enzyme. If you have already worked with Enzyme, you should know that there are shallow and mount for rendering the component. Mount is for rendering the full DOM while shallow will not render the sub component unless you 'find' and 'dive' into it.

Imagine that we have a component with the name of "Com" with another component "SubCom" inside. Here is an example of test:

it("should have classes with name of container and subContainer", () => {
  // test with mount

  // WHEN
  const wrapper = mount(<Com />)

  // THEN
  expect(wrapper.find(".container").length).toEqual(1)
  expect(wrapper.find(".subContainer").length).toEqual(1)
})

it("should have a subComponent", () => {
  // test with shallow

  // WHEN
  const wrapper = shallow(<Com />)
  // THEN
  expect(wrapper.find("SubCom").length).toEqual(1)
  expect(wrapper.find("SubCom").props()).toEqual({ testProp: "testProp" }) // need to find the SubComponent in shallow rendering
})

it("func should be executed if we click on subComponent", () => {
  // GIVEN
  const func = jest.fn()
  const props = { func }

  // WHEN
  const wrapper = mount(<Com {...props} />)
  wrapper.find(".subContainer").simulate("click")

  // THEN
  expect(func).toBeCalled()
})

Link to source code.

2. Test with connected component and HOC (Higher Order Component)

'Connect' in react-redux is a higher order component. I will give two examples about how to test a connected component, and how to test another higher order component like redux-form.

it("Should be able to test connected component", () => {
  // GIVEN
  const store = createStore(() => ({})) // mock store

  // WHEN
  const wrapper = mount(
    <Provider store={store}>
      // in need of a Provider for connected component
      <ConnectedComponent />
    </Provider>
  )

  // THEN
  expect(wrapper.find("div").length).toEqual(1)
})

it("should be able to test redux form", () => {
  // GIVEN
  const store = createStore(() => ({}))

  // WHEN
  const ConnectedReduxFormComponent = reduxForm({ form: "test" })(FormComponent)
  const wrapper = mount(
    <Provider store={store}>
      <ConnectedReduxFormComponent />
    </Provider>
  )

  // THEN
  expect(wrapper.find("form").length).toEqual(1)
})

Link to source code.

3. Test with redux-saga

Redux-saga use generator function, so test with saga function is to play with the "next" in generator function. Every 'yield' should correspond to an execution with 'next()'.

// saga function
function* testGeneratorFunction() {
  yield call(testFunction, "test1")
  yield call(testFunction, "test2")
  yield call(testFunction, "test3")
}

// test
it("should call testFunction", () => {
  // GIVEN
  const gen = testGeneratorFunction()

  // WHEN
  let next = gen.next()
  let effect = call(testFunction, "test1")

  // THEN
  expect(next.value).toEqual(effect) // .next().value return the value from yield

  // WHEN
  next = gen.next()
  effect = call(testFunction, "test2")

  // THEN
  expect(next.value).toEqual(effect)

  // WHEN
  next = gen.next()
  effect = call(testFunction, "test3")

  // THEN
  expect(next.value).toEqual(effect)
})

Link to source code.

4. Test with function who returns promise

Enzyme provides resolves and rejects to help with testing with async, but it is also possible to test the promise by mocking the execution of promise with 'then'.

// the original function
const service = input => {
  return new Promise((resolve, reject) => {
    if (input) {
      resolve("ok")
    } else {
      reject("error")
    }
  })
}

// the test
it("should return ok if input is true with .resolves", () => {
  const promise = service(true)
  return expect(promise).resolves.toBe("ok")
})

it("should return ok if input is true with .then", () => {
  // remember to return the promise in the end
  return service(true).then(data => {
    expect(data).toBe("ok")
  })
})

it("should return error if input is false with .rejects", () => {
  const promise = service(false)
  return expect(promise).rejects.toMatch("error")
})

it("should return error if input is false with .then", () => {
  return service(false).catch(error => {
    expect(error).toBe("error")
  })
})

Link to source code.

5. Test components with react hooks

In order to test hooks, I use react testing library. React testing library is competitor of Enzyme for react integration test. The idea behind this library is to test the DOM by simulating the real user interface. It is like "mount" in Enzyme. (Update 2019/08/27: Enzyme now also supports hooks.)

import React from "react"
import { render, fireEvent } from "@testing-library/react"
import { ComponentWithHooks } from "./componentWithHook"

describe("ComponentWithHooks", () => {
  test("show default text", () => {
    // WHEN
    const { getByText, queryByText } = render(<ComponentWithHooks />)

    // THEN
    expect(getByText("Hello")).toBeInTheDocument()
    expect(queryByText("World")).not.toBeInTheDocument() // query by won't throw https://testing-library.com/docs/dom-testing-library/cheatsheet#queries
  })
  test("show default text", () => {
    // WHEN
    const { queryByTestId } = render(<ComponentWithHooks />)

    // THEN
    expect(queryByTestId("clicked")).not.toBeInTheDocument()

    // WHEN
    fireEvent.click(queryByTestId("button"))

    // THEN
    expect(queryByTestId("clicked")).toBeInTheDocument()
  })
})

Link to source code.

6. Test graphql queries

We can test graphql queries by providing a MockedProvider for mocked query responses. An example will be shown in the next part 'Test custom hooks'.

If the query is made by the hook useQuery and if it is used only once in the component, it is not a bad way to mock useQuery to return the expected result. (For more information: How to mock dependencies in jest: the pitfalls, the tricks and the best practices)

7. Test custom hooks

What is the custom hook ? A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks. Component is for the UI, then the custom hook is for your component logic. It can be how we get and format data, and it can be also how to handle the callbacks.

Without a library, it is not easy to test a custom hook independently because it "lives" in a component. Luckily the library react hooks testing library helps us solve this problem.

Let's take an example of a custom hook which uses two other hooks (useContext and useQuery).

const useMyHook = () => {
  const {id} = useMyContext() // get data from my context
  const {data, error, loading} = useQuery(query, variables: {id}) // use graphql hook to get data
  const formattedData = //... format data
  return formattedData // return data
}


// to test

// the hook use context and query, in this case we need to create wrapper for test
const GQLMockedProvider = ({children}) => (
  <MockedProvider mocks={...}> {/* MockedProvider from apollo to mock query responses */}
    <MyContext.Provider> {/* Wrap context provider for hook wrapper */}
      {children} {/* the children here is necessary, if not the hook won't be rendered*/}
    </MyContext.Provider>
  </MockedProvider>
)


describe('my hook', () => {
  it('should return right data', async () => {
    const {result, waitForNextUpdate} = renderHook(() => useMyHook(), {
      wrapper: GQLMockedProvider, // use the wrapper
    });
    await waitForNextUpdate() // useQuery is async
    expect(result.current).toEqual(...)
  })
})

Thanks for reading !

Back to Blogs|Go to Top