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 Jest, React 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. Therender
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),
});
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 usingcreateDeepFactory
method from @shipfast/webapp-api-client/tests/utils
.Here's an example of how to define a factory:
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.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:
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);
});
});