Testing in O3

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:

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 expect with 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 use query* variants for asserting that an element does not exist.

    • Don't use waitFor to wait for elements that can be queried with screen.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 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 instead to fix Not wrapped in act(...) warnings.

    • Don't use render or renderHook to wrap components in tests.

  • Avoid the test user.

  • 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 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

  • 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: