3

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 have found out there is a setting in useQuery, "retry" that is default to 3 requests. I still don't know how to deal with component retrying requests in my test.

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} />
      ))}
    </>
  );
};

2 Answers 2

16

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();
    })
  });
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, this is what I was looking for!
this is the answer
-2

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.