Skip to main content

Writing tests for webapp

Writing tests for your web application is essential to ensure the correctness and reliability of your product. In this guide, you will go through some of the best practices for testing your web application.

Testing stack

ShipFast comes with an extensive testing stack that allows you to test your components, queries, and mutations. It uses popular testing libraries such as JestReact Testing Library, and Apollo Client to provide a robust and reliable testing environment. It also includes some custom helpers that speeds up and simplify work.

Custom render methods

ShipFast provides a custom render and renderHook methods to simplify the testing process. The render method from @testing-library/react is wrapped with additional functionality to handle setup of providers like the Apollo Client. This method provides a convenient way to render your components within pre-configured test environment.

Here's an example of how to use the custom render method:
import { render } from '../../../../../tests/utils/rendering';

//...
it('should render ', async () => {
render(<Component />);

expect(screen.getByText('Rendered')).toBeInTheDocument();
});

Testing components with queries

In testing environment all queries that are expected to be executed from the components that are being tested should be mocked. To do that you can prepare mocked queries using fill* methods or helper methods and pass them to render method using apolloMocks property.

You can also use the waitForApolloMocks function that is returned from render method to wait for all (or some) mocks to be executed to assert the correct state of the rendered results.

Here's an example of how to test component that expect data from logged user:

import { currentUserFactory, fillCommonQueryWithUser } from '@shipfast/webapp-api-client/tests/factories';
import { Role } from '../../../../../modules/auth/auth.types';
import { render } from '../../../../../tests/utils/rendering';

//...
it('should render ', async () => {
const apolloMocks = [
fillCommonQueryWithUser(
currentUserFactory({
roles: [Role.ADMIN],
})
)
];
const { waitForApolloMocks } = render(<Component />, {
apolloMocks,
});

await waitForApolloMocks();

expect(screen.getByText('Rendered')).toBeInTheDocument();
});

You can also pass a function to the apolloMocks prop to modify the default mocks:

render(<Component />, {
apolloMocks: (defaultApolloMocks) => defaultApolloMocks.concat(customMock),
});
info

To get more information about possible arguments and setup of test providers check the webapp/test/utils/rendering.tsx API reference.

Advanced example

Here's an advanced example of testing a component with queries and routing structure:

import { getLocalePath } from '@shipfast/webapp-core/utils';
import { currentUserFactory, fillCommonQueryWithUser } from '@shipfast/webapp-api-client/tests/factories';
import { composeMockedQueryResult } from '@shipfast/webapp-api-client/tests/utils';

import { RoutesConfig } from '../../../../app/config/routes';
import { Role } from '../../../../modules/auth/auth.types';
import { createMockRouterProps, render } from '../../../../tests/utils/rendering';
import { authConfirmUserEmailMutation } from '../confirmEmail.graphql';

//...
describe('ConfirmEmail: Component', () => {
const user = 'user_id';
const token = 'token';

// Create a component structure with routing that will imitate real app
const Component = () => (
<Routes>
<Route path={getLocalePath(RoutesConfig.confirmEmail)} element={<ConfirmEmail />} />
<Route path={getLocalePath(RoutesConfig.login)} element={<span>Login page mock</span>} />
</Routes>
);

it('should show success message and redirect to login ', async () => {
// Configure mocks:
const apolloMocks = [
// 1. Common query mock (logged-in user)
fillCommonQueryWithUser(
currentUserFactory({
roles: [Role.ADMIN],
})
),
// 2. Confirm email mutation with the correct user, token and successful result
composeMockedQueryResult(authConfirmUserEmailMutation, {
variables: {
input: { user, token },
},
data: {
confirm: {
ok: true,
},
},
}),
];
// Configure initial router props
const routerProps = createMockRouterProps(RoutesConfig.confirmEmail, { user, token });

// Call render method from `webapp/src/tests/utils/rendering.tsx`
const { waitForApolloMocks } = render(<Component />, {
routerProps,
apolloMocks,
});

// Wait for all queries
await waitForApolloMocks();

// Assert the final expected result
expect(screen.getByText('Login page mock')).toBeInTheDocument();
});
});

Defining factories

ShipFast provides a way to define factories that generate mock data for your tests using createDeepFactory method from @shipfast/webapp-api-client/tests/utils.

Here's an example of how to define a factory:
packages/webapp-libs/webapp-documents/src/tests/factories/document.ts
import { DocumentDemoItemType } from '@shipfast/webapp-api-client/graphql';
import {
createDeepFactory,
makeId,
} from '@shipfast/webapp-api-client/tests/utils';
//...
export const documentFactory = createDeepFactory<Partial<DocumentDemoItemType>>(() => ({
id: makeId(32),
createdAt: new Date().toISOString(),
file: {
name: `${makeId(32)}.png`,
url: `http://localhost/image/${makeId(32)}.png`,
},
}));

Later in tests or storybook you can import and use the factory:

import { documentFactory } from '@shipfast/webapp-documents/src/tests/factories/document'
//...
const document = documentFactory();
// or
const document = documentFactory({ file: { name: 'image.png', url: 'http://example/image.png' } })

Defining and using fill* methods

ShipFast provides a way to define fill* methods that generate mock queries for Apollo MockedProvider. These methods can be used to create mock query results that match the query schema.
info

Check testing helper methods API reference on how to compose mocked GraphQL query results.

Here's an example of how to define a fill* method:

packages/webapp-libs/webapp-documents/src/tests/factories/document.ts
import { composeMockedListQueryResult } from '@shipfast/webapp-api-client/tests/utils';
import { times } from 'ramda';

import { documentsListQuery } from '../../routes/documents/documents.graphql';
//...
export const fillDocumentsListQuery = (data = times(() => documentFactory(), 3)) => {
return composeMockedListQueryResult(documentsListQuery, 'allDocumentDemoItems', 'DocumentDemoItemType', { data });
};

Usage of defined fill* method in unit test:

import { times } from 'ramda';
import { documentFactory, fillDocumentsListQuery } from '../../../tests/factories';
import { render } from '../../../tests/utils/rendering';

//...
describe('Documents: Component', () => {
it('should render maximum size state', async () => {
const documentsLength = 3;
const generatedDocs = times(() => documentFactory(), documentsLength);

const mockRequest = fillDocumentsListQuery(generatedDocs);
render(<Documents />, { apolloMocks: (defaultMocks) => defaultMocks.concat(mockRequest) });

expect(await screen.findAllByRole('link')).toHaveLength(documentsLength);
expect(screen.getAllByRole('listitem')).toHaveLength(documentsLength);
});
});