Testing in O3
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 and React Testing Library for unit and integration testing.
Playwright for e2e testing.
The following are some general guidelines for testing:
Unit and integration testing
Avoid these common testing pitfalls:
Don't use the wrong assertion. Install and use @testing-library/jest-dom to extend Jest's
expectwith assertions for testing React components.Don't use the wrong query. Use this resource 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
waitForto 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 and eslint-plugin-jest-dom.
Don't use the
cleanupfunction. RTL takes care of cleaning up after each test.Use
screento 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 instead to fixNot wrapped in act(...)warnings.Don't use
renderorrenderHookto wrap components in tests.
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 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
beforeEachhooks. 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
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.ts
import { 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:
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(); }); });
Explainer Videos
Course
Take the Test Automation course (includes quiz questions and certificate) on the OpenMRS Academy: