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. - Use the 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. - Use
screen
to query and debug elements. This is the recommended way to find elements in your tests as it ensures you're working with the current document and provides better error messages.
// ❌ const { getByText } = render(<MyComponent />); const element = getByText(/hello/i); // ✅ render(<MyComponent />); const element = screen.getByText(/hello/i);
- 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(); }); });
-