Testing - amplimindcc/frontend GitHub Wiki

Tools and Packages

  • Vitest: Testing framework for Vite projects.
  • jsdom: JavaScript implementation of the DOM that emulates the browser environment for Vitest
  • React Testing Library: For testing React components.
  • User Event: Part of the React Testing Library that simulates user interactions
  • Mock Service Worker: For mocking backend API calls



Configuration

  • vite.config.ts: Configure Vitest
  • testSetup.ts: Global setup for tests
  • package.json: Test related scripts
  • pipeline.yml: Further information on CI pipeline



Setup

Install dependencies

npm install



Run Tests

npm run test



Structure

Test Files

Test files follow the structure *.test.jsx and can be found in the corresponding page/component folders. For example, test cases for the login page are located in the Login.test.jsx file under ./src/pages/Login.

When testing pages, it is necessary to import all components that it is reliant on.

Example:

render(
    <>
        <AuthProvider>
            <LangProvider>
                <Router>
                    <ProjectStart /> // actual page that we test
                </Router>
                <ToastContainer />
            </LangProvider>
        </AuthProvider>
    </>
);

This rendering structure is applied to all pages. <AuthProvider> enables rendering and access to route protected pages. <LangProvider> gives the text to the components. <ToastContainer> returns/display needed responses we have to test against sometimes. <Router> is needed since App.tsx is based on route structure and we make use of useNavigate() hook in all components.


API Mocks

API mocks are located in ./mocks/api.ts and served with ./mocks/server.ts. The server file server.ts is imported inside of testSetup.ts and will be executed before each complete test run (npm run test). Furthermore the server handlers will be reset after each single test case. This reset is useful in case of any API handler overrides inside the test cases (more about handler override below).

Creating and Maintaining API Mocks

Mocks inside ./mocks/api.ts are global and will be called by default from the frontend if needed. Global handlers only support one API response at a time. For this reason, you cannot register the same handler (same HTTP type and route) twice with different responses.

Example:

./mocks/api.ts contains following handler:

export const handlers = [
    http.post(`${baseURL}/v1/auth/login`, () => {
        return new HttpResponse(null, {
            status: StatusCodes.OK,
        });
    })
];

We cannot define the same handler with a different response type:

export const handlers = [
    // will be overridden
    http.post(`${baseURL}/v1/auth/login`, () => {
        return new HttpResponse(null, {
            status: StatusCodes.OK,
        });
    }),

    // will override
    http.post(`${baseURL}/v1/auth/login`, () => {
        return new HttpResponse(null, {
            status: StatusCodes.FORBIDDEN,
        });
    })
];

The second handler is identical in HTTP type and route, therefore it overrides the first handler.

Override API Mocks

Overriding should be used to test frontend against alternative response types that could not be defined in the global ./mocks/api.ts file (no duplicate handlers). Overriding is done by importing the server in the page/component testfile itself and redefining the needed handlers with server.use().

Example:

./mocks/api.ts contains following handler:

export const handlers = [
    http.post(`${baseURL}/v1/auth/login`, () => {
        return new HttpResponse(null, {
            status: StatusCodes.OK,
        });
    })
];

Override handler in ./src/pages/Login/Login.test.jsx:

test('unsuccessful login', async () => {
    server.use(
        // overrides handler in ./mocks/api.ts
        http.post(`${baseURL}/v1/auth/login`, () => {
            return new HttpResponse(null, {
                status: StatusCodes.FORBIDDEN,
            });
        })
    );

    // ...
});

Mocking the Browser Language

In most of the test files you will find yourself searching for text snippets. Because the whole project uses internationalization, you will first need to mock the browser language that is grabbed by the <LangProvider> component. You can achieve this by setting the according property in the navigator like this:

Object.defineProperty(navigator, 'language', {
    value: 'en',
    configurable: true,
});

This block is best used in the beforeEach() loop so every test case uses the mocked navigator.


Mocking URL params

Some of the tested pages require URL parameters like tokens to be provided. The best approach to mock these is by using <MemoryRouter> instead of the normal <BrowserRouter> in the render function:

<MemoryRouter initialEntries={[`/path/${token}`]}>
    <Routes>
        <Route
            path="/path/:token"
            element={<TestedPage />}
        />
    </Routes>
</MemoryRouter>

Mocking setTimeout()

If you need to mock a setTimeout(), you can achieve this by using:

vi.useFakeTimers();
vi.advanceTimersByTime(ms);

To clear the mocked timers, you can use:

vi.clearAllTimers();
vi.useRealTimers();

Questions regarding testing: @cwsyth, @MysterionAutotronic

⚠️ **GitHub.com Fallback** ⚠️