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.
    • 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. Use screen for querying elements and debugging assertions.
    • 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();
        });
      });