Search code examples
javascriptreactjsjestjsmockingfetch

Can't mock function multiple times react, testing


I want to test my component:

const Courses: React.FC = () => {
  const { data, error } = useSWR(
    'some url...',
    fetcher
  );

  console.log(data, error);

  if (error) {
    return (
      <CoursesContainer>
        <Error>Something went wrong.</Error>
      </CoursesContainer>
    );
  }

  if (!data) return <Loader title="loader" />;

  return (
    <CoursesContainer>
      <CollapsibleTable courses={data} />
    </CoursesContainer>
  );
};

export default Courses;

but I don't know why I can't mock it to return different value for each test. I've tried that:

jest.mock('../../utils/fetcher', () => ({
  fetcher: jest
    .fn()
    .mockReturnValue('default')
    .mockReturnValueOnce('first call')
    .mockReturnValueOnce('second call'),
  readData: jest.fn(),
}));

test('Basic render. fetch pending', async () => {
  const component = render(<Courses />);
  await waitFor(() => component.findByTitle('loader'));
  expect(component.baseElement).toMatchSnapshot();
});

test('Basic render, fetch success', async () => {
  const component = render(<Courses />);
  await waitFor(() => component.findByText('CollapsibleTable'));
  expect(component.baseElement).toMatchSnapshot();
});

test('Basic render, fetch error', async () => {
  const component = render(<Courses />);
  await waitFor(() => component.findByText('Something went wrong.'));
  expect(component.baseElement).toMatchSnapshot();
});

and that doesn't work well. For each of tests there is only first call console.log() - The console.log(data, error); from Courses.tsx. The feedback from jest:

  console.log
    undefined undefined

      at Courses (src/components/Courses.tsx:14:11)

  console.log
    first call undefined

      at Courses (src/components/Courses.tsx:14:11)

  console.log
    first call undefined

      at Courses (src/components/Courses.tsx:14:11)

  console.log
    first call undefined

      at Courses (src/components/Courses.tsx:14:11)

And of course the third test (Basic render, fetch error) is failed cos of that.

I can't use spyOn() instead, cos of my fetcher is separate function whithout object.

@@ UPDATE @@

There are my fetcher and readData functions:

const fetcher = (url: string) => {
  return fetch(url)
    .then((response) => response.json())
    .then((data: Array<IFetchData>) => readData(data));
};

const readData = (data: Array<IFetchData>) => {
  let myData: Array<ICourse> = [];

  [ there are some simple operations which create new myData array with 
    properties which I need (there is not any async operations)]

  return myData;
};

Solution

  • You have to give mock implementation for readData as well.

    According to jest specification,

    We can create a mock function with jest.fn(). If no implementation is given, the mock function will return undefined when invoked.


    This will make more sense about your test.

     await waitForElementToBeRemoved(() => component.getByTitle('loader'));
    

    We're waiting for the loader title to be removed which ensures that the title shows up in the first place and now it is removed when loader is completed.

    jest.mock('../../utils/fetcher', () => ({
      fetcher: jest
        .fn()
        .mockResolvedValue('default')
        .mockResolvedValueOnce('first call')
        .mockResolvedValueOnce('second call'),
      readData: jest.fn().mockResolvedValue('Read call'), //provide reseolve value
    //jest.fn() returns undefined when we dont't provide implementation
    }));
    
    test('Basic render. fetch pending', async () => {
      const component = render(<Courses />);
      await waitForElementToBeRemoved(() => component.getByTitle('loader'));
      expect(component.baseElement).toMatchSnapshot();
    });
    
    test('Basic render, fetch success', async () => {
      const component = render(<Courses />);
      await waitForElementToBeRemoved(() => component.getByText('CollapsibleTable'));
      expect(component.baseElement).toMatchSnapshot();
    });
    
    test('Basic render, fetch error', async () => {
      const component = render(<Courses />);
      await waitForElementToBeRemoved(() => component.getByText('Something went wrong.'));
      expect(component.baseElement).toMatchSnapshot();
    });
    

    @Updated answer

    Sorry to say that you can't achieve what you want. The reason is the render function is called only once in your test case so it means that the fetcher and readData API will call only once.

    const mockFn = jest.fn();
    jest.mock('../../utils/fetcher', () => ({
      fetcher: mockFn.mockResolvedValueOnce('first call'),
      readData: mockFn.mockResolvedValue(['Read call']), // returns array
    }));
    
    test('Basic render. fetch pending', async () => {
      const component = render(<Courses />);
      await waitForElementToBeRemoved(() => component.getByTitle('loader'));
      expect(mockFn).toHaveBeenCalledTimes(1); // test passed
      expect(component.baseElement).toMatchSnapshot();
    });
    

    Even your provide mockResolvedValueOnce again it will give undefined as render function doesn't get a chance to call the second time to mock version of fetcher and readData.