ann.piv
ann.piv

Reputation: 771

Problem with testing for error in React component using react-query and axios. Test with React Testing Library, Jest, msw

I test a react component that should display either spinner, error or a list of fetched recipes. I managed to test this component for loading and when it successfully get data. I have a problem with testing error. I created test server with msw, that has route handler that returns error. I use axios to make requests to the server. I think the problem is here: axios makes 3 requests and until last response useQuery returns isLoading=true, isError=false. Only after that it returns isLoading=false, isError=true. So in my test for error screen.debug() shows spinner, and errorMessage returns error because it does not find a rendered element with text 'error', that component is supposed to show when error occured. What can I do about that? I run out of ideas.


EDIT:


I'm new to react testing and Typescript.

From RecipeList.test.tsx:

import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { QueryClient, QueryClientProvider, QueryCache } from 'react-query';

import RecipeList from './RecipeList';

const server = setupServer(
  rest.get(`${someUrl}`, (req, res, ctx) =>
    res(ctx.status(200), ctx.json(data))
  )
);

const queryCache = new QueryCache();
const RecipeListWithQueryClient = () => {
  const client = new QueryClient();
  return (
    <QueryClientProvider client={client}>
      <RecipeList />
    </QueryClientProvider>
  );
};

// Tests setup
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
afterEach(() => {
  queryCache.clear();
});

describe('RecipeList', () => {
  //some tests that pass

  describe('while error', () => {
    it('display error message', async () => {
      server.use(
        rest.get(`${someUrl}`, (req, res, ctx) => res(ctx.status(404)))
      );
      render(<RecipeListWithQueryClient />);
      // FOLLOWING CODE RAISE ERROR
      const errorMessage = await screen.findByText(/error/i);
      await expect(errorMessage).toBeInTheDocument();
    });
  });
});

Component code:

From RecipeList.tsx:

import { fetchRecipes } from 'api';

const RecipeList: React.FC = () => {
  const { data, isLoading, isError } = useQuery<
    RecipeDataInterface,
    Error
  >('recipes', fetchRecipes);;

  if (isLoading) {
    return (
      <Spinner>
        <span className="sr-only">Loading...</span>
      </Spinner>
    );
  }

  if (isError) {
    return (
      <Alert >Error occured. Please try again later</Alert>
    );
  }

  return (
    <>
      {data?.records.map((recipe: RecipeInterface) => (
        <RecipeItem key={recipe.id} recipe={recipe.fields} />
      ))}
    </>
  );
};

Upvotes: 1

Views: 14369

Answers (2)

Adam Kopciński
Adam Kopciński

Reputation: 545

I used QueryClient to disable retry. And setupServer to mock network connection

    import { QueryClient, QueryClientProvider} from 'react-query';
    import { setupServer } from 'msw/node'
    import { rest } from 'msw'

    
const server = setupServer( 
  rest.get('https://reqres.in/api/users', (req, res, ctx) => {      
      return res(
        ctx.status(200),
        ctx.json(userSuccessResponseJson()
            ),
      )
    }),)

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


const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retryDelay: 1,
      retry:0,
    },
  },
})
const Wrapper = ({ children }) => (
  <QueryClientProvider client={queryClient}>
    {children}
  </QueryClientProvider>
);



  test('Renders Error text when backend server response with error', async () => {
    server.use(
      rest.get('https://reqres.in/api/users', (req, res, ctx) => {
        return res(ctx.status(403))
      })
    );
  
    render(<Wrapper><CompanyList />)</Wrapper>);
  
    const linkElement = screen.getByText(/Loading/i);
    expect(linkElement).toBeInTheDocument();
  
    await waitFor(() => {      
      const fetchedText = screen.getByText(/Error:/i);      
      expect(fetchedText).toBeInTheDocument();
    })
  });

Upvotes: 14

ann.piv
ann.piv

Reputation: 771

The solution I found is to set individual timeout value for the test.

In RTL for waitFor async method according to docs :

"The default timeout is 1000ms which will keep you under Jest's default timeout of 5000ms."

I had to set timeout option for RTL async function, but it was not enough as I was getting an error:

"Exceeded timeout of 5000 ms for a test.Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test." I had to change timeout value set by Jest for the test. It can be done in two ways as described in this comment:

" 1. Add a third parameter to the it. I.e., it('runs slow', () => {...}, 10000) 2. Write jest.setTimeout(10000); in a file named src/setupTests.js, as specified here."

Finally my test looks like this:


  it('display error message', async () => {
    server.use(
      rest.get(getUrl('/recipes'), (req, res, ctx) =>
        res(ctx.status(500), ctx.json({ errorMessage: 'Test Error' }))
      )
    );

    render(<RecipeListWithQueryClient />);

    expect(
      await screen.findByText(/error/i, {}, { timeout: 10000 })
    ).toBeInTheDocument();
  }, 10000);

Different async methods could be used here like waitFor and waitForElementToBeRemoved and they all can take { timeout: 10000 } object as an argument.

This solution works for me, however I would like to improve it, to avoid the tests taking so much time. I just didn't figure out yet how to do it.

Upvotes: -2

Related Questions