Docs
Coding conventions
Testing

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:

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 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 (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 fix Not wrapped in act(...) warnings.
    • Don't use render or renderHook to wrap components in tests.
  • Avoid the test user (opens in a new tab).

  • 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.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 (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();
        });
      });