Kategorier
Code testing

Tips and tricks for testing with Jest

Writing tests can be daunting when starting out, it’s hard to know exactly what to test and then learning the API for your test tool.

I wanted to share some small tips that can be useful when starting out.

expect.objectContaining()

In some cases you are only interested in the value of one or just a few properties in an object. To check for a specific property you can use expect.objectContaining to check if the object contains a property with the expected value.

In the code below we’re checking if a report dialog function has been called with the users name and email.
The actual object is much larger but we don’t really care about the other properties, in this case the user information is the moving parts in the object.

expect(showReportDialog).toHaveBeenCalledWith(
  expect.objectContaining({
    user: {
      name,
      email,
    }
  })
);

expect.anything()

Callback functions or randomly generated values can sometimes be a hassle to handle in tests since they might change, but it is possible to ignore specific properties or arguments using expect.anything.

function loadScript(scriptUrl:string, callback:() => unknown) { ... }

When testing the above function we ’re not interested in the callback function and only wants to check if loadScript have been called with the correct script.

it("should call loadScript", () => {
  someFunctionUsingLoadScript();

  expect(loadScript).toHaveBeenCalledWith(
    "script.js",
    expect.anything()
  );
}

expect.anything does not match null or undefined values

expect.any()

Another way to match more broadly is expect.any(constructor) where you can accept any match based on the constructor being passed to it.

expect(someFn).toHaveBeenCalledWith({
  someNumber: expect.any(Number),
  someBoolean: expect.any(Boolean),
  someString: expect.any(String)
});

expect.assertions()

When doing asynchronous tests it can be helpful to make sure that all assertions have been run when the test ends.
The expect.assertions(Number) ensures that the correct number of assertions have been made.

test('prepareState prepares a valid state', () => {
  expect.assertions(1);
  prepareState((state) => {
    expect(validateState(state)).toBeTruthy();
  })
  return waitOnState();
})

test.each

For some unit tests you may want run the same test code with multiple values. A great way to do this is using the test.each function to avoid duplicating code.

Inside a template string we define all values, separated by line breaks, we want to use in the test. The first line is used as the variable name in the test code.

test.each`
  someId
  ${undefined}
  ${null}
  ${""}
`("$someId should reject promise", async ({ someId}) => {
  expect.assertions(1);
  await expect(someFn(someId))
    .rejects.toEqual(errorMessage);
});

Multiple input variables can be added separated by the pipe (|) character.

test.each`
  someId       | anotherValue
  ${undefined} | ${a}
  ${null}      | ${b}
  ${""}        | ${c}
`("$someId with $anotherValue should reject promise", async ({ someId, anotherValue }) => {
  expect.assertions(1);
  await expect(someFn(someId, anotherValue))
    .rejects.toEqual(errorMessage);
});

Note: it is also possible to define the values as arrays, read more in the official documentation.

jest.requireActual

This is just a reminder never to forget adding jest.requireActual when mocking libraries. If you do forget, it can lead to weirdness that may take several hours to solve (talking from personal experience here 😁).

So what does it do?

When mocking a library you may only want to mock a specific function of the library and keep the rest of the library intact.

jest.mock("@material-ui/core", () => ({
  ...jest.requireActual("@material-ui/core"),
  useMediaQuery: jest.fn()
}));

So in the code above we create a new mock object, using jest.requireActual to spread all the functions of the library and only mock useMediaQuery in this case.

Kategorier
Frontend

How to mock the Redux useSelector hook

There is an official way of using RTL with redux as some people pointed out, but for small quick tests mocking useSelector may still be of use. 🙄

Recently I finally made the switch from Enzyme to React testing library (RTL) which also means that instead of rendering components using shallow like Enzyme proposes, with React testing library the whole component and its child components is rendered, much like Enzymes mount.

The switch to RTL coupled with using hooks instead of HOCs when using Redux got me writing a lot of new component tests but I did run in to some problem when I tried to use the useSelector-hook from Redux multiple times expecting different responses.

The component that I wanted to test as a search component that made calls similar to this:

const MySearchComponent = () => {
  const { query, rows } = useSelector((state) => 
    state.config);

  const {
      items,
      hasMore
    } = useSelector((state) => state.search);

  return (...)
}

useSelector takes a callback function that takes the state as an argument and returns a slice of the state.

So my first approach when trying to test the component was to send two different responses.

jest.mock("react-redux", () => ({
  useSelector: jest.fn()
    .mockReturnValueOnce(mockConfigState)
    .mockReturnValueOnce(mockSearchState)
}));

describe("MySearchComponent", () => {
  afterEach(() => {
    useSelector.mockClear();
  });
  it("should render", () => {
    const { getByTestId } = render(<MySearchComponent />);
    expect(...)
  });
});

Which worked fine until I realised that a child component also calls useSelector and therefore crashed. 😱

I knew I needed something that would support all possible selectors that I needed but still could be modified on a test by test basis.
I had a mock state ready, but not the method to alter and inject it.
Until I ran across jest.fn().mockImplementation

The solution to my problems

useSelector takes a callback as its argument and all I had to do was to call that callback with a compatible state that would satisfy all my components needs and they would do the rest as implemented.

jest.mock("react-redux", () => ({
  useSelector: jest.fn()
}));

describe("MySearchComponent", () => {
  beforeEach(() => {
    useSelector.mockImplementation(callback => {
      return callback(mockAppState);
    });
  });
  afterEach(() => {
    useSelector.mockClear();
  });
  it("should render a query", () => {
    const { getByTestId } = render(<MySearchComponent />);
    expect(getByTestId("query_testId").textContent)
      .toEqual(mockAppState.config.query)
  });
  it("should not render if query is empty", () => {
      const localMockState = {
        ...mockAppState,
        config: {
          ...mockShoppingState.config,
          query: ""
        }
      };
      useSelector.mockImplementation(callback => {
        return callback(localState);
      });
    const { queryByTestId } = render(<MySearchComponent />);
    expect(queryByTestId("query_testId")).toBeNull();
  });
});

So in the code above I mock useSelector from the react-redux npm package and replaces it with a function that executes any given callback function with my mocked state as an argument. This is done before every test.

In the second test I create a second mocked state that I want to use for just that test so I override useSelector to make sure it uses my updated state instead of the default mock state.

Parting words

I hope this helped someone to learn a little bit more about how to test their code and what can be achieved with jest and tools like RTL (which is great, try it!)

All typos my own and please leave a comment if you have a question or something does not make sense.