Testing
We use a combination of unit testing, integration testing, and e2e testing to ensure the reliability and maintainability of O3. We use the following tools:
- Jest (opens in a new tab) and React Testing Library (opens in a new tab) for unit and integration testing.
- Playwright (opens in a new tab) for e2e testing.
The following are some general guidelines for testing:
Unit and integration testing
-
Avoid these common testing pitfalls (opens in a new tab):
- Don't use the wrong assertion. Install and use @testing-library/jest-dom (opens in a new tab) to extend Jest's
expect
with assertions for testing React components. - Don't use the wrong query. Use this resource (opens in a new tab) to choose the right query for the element you want to test.
- Don't use
query*
variants for anything except checking if an element exists. Only usequery*
variants for asserting that an element does not exist. - Don't use
waitFor
to wait for elements that can be queried withscreen.find*
.
// ❌ const submitButton = await waitFor(() => screen.getByRole('button', { name: /submit/i }), ) // ✅ const submitButton = await screen.findByRole('button', { name: /submit/i })
- Don't pass an empty callback to
waitFor
.
// ❌ await waitFor(() => {}) expect(window.fetch).toHaveBeenCalledWith('foo') expect(window.fetch).toHaveBeenCalledTimes(1) // ✅ await waitFor(() => expect(window.fetch).toHaveBeenCalledWith('foo')) expect(window.fetch).toHaveBeenCalledTimes(1)
- Don't add
aria-*
,role
, and other accessibility attributes to elements unless the application actually uses them. - Don't use Testing Library's ESLint plugins: eslint-plugin-testing-library (opens in a new tab) and eslint-plugin-jest-dom (opens in a new tab).
- Don't use the
cleanup
function. RTL takes care of cleaning up after each test. - Don't use
screen
to query for elements. Usescreen
for querying elements and debugging assertions. - Don't unnecessarily wrap things in
act
. Follow this guide (opens in a new tab) instead to fixNot wrapped in act(...)
warnings. - Don't use
render
orrenderHook
to wrap components in tests.
- Don't use the wrong assertion. Install and use @testing-library/jest-dom (opens in a new tab) to extend Jest's
-
Avoid testing implementation details. Instead, test the component's public API. This makes it easier to refactor the component without having to rewrite the tests.
-
When stubbing out functionality from
@openmrs/esm-framework
, follow the mocking patterns described here. -
Write fewer, more comprehensive tests that cover complete scenarios, rather than many small isolated tests. Read this blog post by Testing Library author, Kent C. Dodds (opens in a new tab) for more information.
- Group related test scenarios together.
- Build up complex scenarios progressively.
- Use test hooks (
beforeEach
,afterEach
,beforeAll
,afterAll
) to reduce duplication.
-
Don't clear mocks in
beforeEach
hooks. Most mocks should be cleared automatically by the testing framework if you have this set in your root-level jest config:// jest.config.js clearMocks: true,
-
Don't mock components. Instead, render them with the real implementation. This makes it easier to catch regressions when refactoring components.
// ❌ jest.mock('./my-component', () => ({ __esModule: true, default: () => <div>My Component</div>, }));
// ✅ render(<MyComponent />);
E2E testing
-
Follow the e2e testing best practices (opens in a new tab) outlined in the Playwright docs.
-
Structure large test suites using page object models:
-
Create separate classes for each major page/component
-
Encapsulate selectors and common actions
e2e/pages/conditions-page.tsimport { type Page } from '@playwright/test'; export class ConditionsPage { constructor(readonly page: Page) {} readonly conditionsTable = () => this.page.getByRole('table', { name: /conditions summary/i }); async goTo(uuid: string) { await this.page.goto(`/openmrs/spa/patient/${uuid}/chart/Conditions`); } }
-
-
Follow Playwright's E2E testing best practices (opens in a new tab):
-
Use web-first assertions instead of manual waits
-
Implement proper error handling and retry mechanisms
-
Use test fixtures for common setup/teardown
test.beforeEach(async ({ api }) => { patient = await generateRandomPatient(api); }); test('Record, edit and delete a condition', async ({ page }) => { const conditionsPage = new ConditionsPage(page); const headerRow = conditionsPage.conditionsTable().locator('thead > tr'); const dataRow = conditionsPage.conditionsTable().locator('tbody > tr'); await test.step('When I go to the Conditions page', async () => { await conditionsPage.goTo(patient.uuid); }); await test.step('And I click on the `Record conditions` button', async () => { await conditionsPage.page.getByText(/record conditions/i).click(); }); await test.step('Then I should see the conditions form launch in the workspace', async () => { await expect(conditionsPage.page.getByText(/record a condition/i)).toBeVisible(); }); });
-