4

I have created a custom hook called useCity. It is wrapping an API call made using useSWR.

Here is the code for hook:

import useSWR from 'swr';

import { City } from '../interfaces';
import { BASE_URL } from '../../config';

interface CitiesResponse {
  data?: {
    records: {
      fields: {
        city: string;
        accentcity: string;
      }
    }[]
  },
  error?: {
    message: string;
  }
};

interface Props {
  start?: number;
  rows: number;
  query?: string;
  sort?: 'population';
  exclude?: string[];
}

const useCity = ({ start = 0, rows, query, sort, exclude }: Props) => {
  const params = [`start=${start}`, `rows=${rows}`];
  if (query) params.push(`q=${query}`);
  if (sort) params.push(`sort=${sort}`);
  if (exclude && exclude.length > 0) params.push(...exclude.map(city => `exclude.city=${city}`))

  const { data, error }: CitiesResponse = useSWR(
    `${BASE_URL.CITIES_SERVICE}?dataset=worldcitiespop&facet=city&${params.join('&')}`,
    { revalidateOnFocus: false,  }
  );

  const cities: City[] = data?.records.map(record => ({
    name: record.fields.city,
    title: record.fields.accentcity,
  })) || [];

  return {
    cities,
    loading: !error && !data,
    error,
  };
};

export default useCity;

Now, I need to test the hook. So, I tried using msw and @testing-library/react-hooks.

Here is my try:

const server = setupServer(
  rest.get(BASE_URL.CITIES_SERVICE, (req, res, ctx) => {
    const start = req.url.searchParams.get('start');
    const rows = req.url.searchParams.get('rows');
    const query = req.url.searchParams.get('query');
    const sort = req.url.searchParams.get('sort');
    const exclude = req.url.searchParams.getAll('exclude.city');

    const getReturnVal: () => DatabaseCity[] = () => {
      // i will write some code that assumes what server will return
    };


    return res(
      ctx.status(200),
      ctx.json({
        records: getReturnVal(),
      }),
    );
  }),
  ...fallbackHandlers,
);

beforeAll(() => server.listen());
afterEach(() => {
  server.resetHandlers();
  cache.clear();
});
afterAll(() => server.close());


it('should return number of cities equal to passed in rows', async () => {
  const wrapper = ({ children } : { children: ReactNode }) => (
    <SWRConfig value={{ dedupingInterval: 0 }}>
      {children}
    </SWRConfig>
  );

  const { result, waitForNextUpdate, } = renderHook(() => useCity({ rows: 2 }), { wrapper });
  const { cities:_cities, loading:_loading, error:_error } = result.current;
  expect(_cities).toHaveLength(0);
  
  await waitForNextUpdate();
  
  const { cities, loading, error } = result.current;
  expect(cities).toHaveLength(2);
});

I assme that the test case will pass once I implement the mock function.

But I don't know if this is the right approach to test such a hook. I am a frontend developer, is this my responsibility to test that API call?

I am new to writing test cases that involves API calls. Am I going in the right direction? I don't know what this kind of tests are called. If someone can tell me the kind of the test I am perfoming, then it will help me google for the solutions instead of wasting other developer's time to answer my questions.

Vishal
  • 6,238
  • 10
  • 82
  • 158

2 Answers2

5

Looks like you are on the right track.

Your useCity hook does basically 2 things that you can validate in tests:

  1. builds an url
  2. converts the cities to another format

You can validate useSWR is called with the correct url by using a spy:

import * as SWR from 'swr';

jest.spyOn(SWR, 'default'); // write this line before rendering the hook.
expect(SWR.default).toHaveBeenCalledWith(expectedUrl, {}); // pass any options that were passed in actual object

You can validate useCities returns correct cities by

const { cities } = result.current;
expect(cities).toEqual(expectedCities);

I am a frontend developer, is this my responsibility to test that API call?

I think that is up to you to find the answer. I personally see as my responsibility to test any code that I write--that of course is not a dogma and is context sensitive.

I don't know what this kind of tests are called. If someone can tell me the kind of the test I am perfoming, then it will help me google for the solutions

There might not be a clear answer for this. Some people would call it unit testing (since useCities is a "unit"). Others might call it integration testing (since you test useCities and useSWR in "integration").

Your best bet would be to google things like "how to test react hooks" or "how to test react components". The RTL docs are a good place to start.


Extra notes

I personally almost never test hooks in isolation. I find it easier and more intuitive to write integration tests for the components that use the hooks.

However, if your hook is going to be used by other projects, I think it makes sense testing them in isolation, like you are doing here.

Vishal
  • 6,238
  • 10
  • 82
  • 158
Doug
  • 5,661
  • 2
  • 26
  • 27
  • 1
    Thank you for the great answer. Now I understood how should I test custom hooks. I asked this question: `I am a frontend developer, is this my responsibility to test that API call?`, because I thought I had to build exact same function that API has and then I need to test that. But I got my answer. I can test the generated url and then I can test the basic response. No need to write the function. – Vishal Nov 20 '20 at 13:15
  • Also, can you please explain me this line: `jest.spyOn('swr', 'default');`. What is first parameter? name of the library? And what is 2nd parameter? – Vishal Nov 20 '20 at 13:16
  • Sorry to bother you. I got my answer here: https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname – Vishal Nov 20 '20 at 13:32
  • @Vishal I hope it helped! Sorry, I noticed that line was a bit confusing. I actually haven't tested it myself, but the idea is to spy on the default export from the `swr` library--that way you can verify it is called correctly. I am not 100% sure if I got the syntax correct. – Doug Nov 20 '20 at 14:12
  • No problems! I will try to write the test case and if I need to change anything, I will post here, so you can update your answer and other developers in future can take advantage of it. :) – Vishal Nov 20 '20 at 15:03
  • I tried the code, but when trying to spyOn swr as mentioned in the answer, I get an error: `Cannot spyOn on a primitive value; string given`. I have tried to search on google, but I was unable to find a solution. If you can help solving this, it will be great. – Vishal Nov 20 '20 at 17:11
  • I have updated your answer after trying some different ways and looks like the code that I posted works! – Vishal Nov 20 '20 at 22:51
  • Though I have the problems in checking 1st parameter only. I only want to check the url. Is there any way around that? – Vishal Nov 20 '20 at 22:52
  • Thank you for updating the answer! For verifying 1st parameter only you could try `.toHaveBeenCalledWith(expectedUrl, expect.anything())` or `expect.any(Object)`. Check [this](https://jestjs.io/docs/en/expect#expectanything) out – Doug Nov 21 '20 at 00:16
  • Thank you for the quick reply. I will try that now. I am also facing another issue with another component while testing. I will post a new question and will let you know. Please suggest if you get some time. I will post the link after I post the question. – Vishal Nov 21 '20 at 00:29
  • I have tried to pass `expect.anything()` as 2nd parameter, but sometimes if I do not pass 2nd parameter to the function call, then test fails. If there is any way to say that 2nd parameter is optional and if present, it should be an Object, then test will pass. – Vishal Nov 21 '20 at 02:22
  • Also, I have posted a new question, where I am facing difficulty to test it. Can you please check that if you get some time? Here is the link: https://stackoverflow.com/questions/64939196/how-can-i-test-if-a-prop-is-passed-to-child – Vishal Nov 21 '20 at 02:25
0

If you want to mock the entire SWR, you can do, the challenge part is to dodge the SWRResponse.


import { render, screen, waitFor } from '@testing-library/react';
import * as SWR from 'swr';


...

  it('should render profile', async () => {
    jest
      .spyOn(SWR, 'default')
      .mockImplementation(() => ({ data: identifyProfile, isValidating: false, mutate: () => Promise.resolve() }));

    render(<MyProfileSection />);

    await waitFor(() => {
      expect(screen.getByText('John Duo')).toBeInTheDocument();
    });
  });
windmaomao
  • 7,120
  • 2
  • 32
  • 36