Unit & Integration Tests (OpenMRS 3)

What are Unit & Integration Tests?

Unit tests and integration tests are two types of software testing methods used in the development process to ensure the quality and reliability of software products.

Unit tests are focused on testing individual components or units of code, such as functions or methods, in isolation from the rest of the system. The purpose of unit tests is to verify that each unit of code behaves as expected and meets its specifications. Unit tests are typically automated and executed frequently during the development process to catch issues early and prevent regressions.

Integration tests, on the other hand, test the interactions and dependencies between different units of code or subsystems. Integration tests are designed to identify defects in the interfaces and interactions between components and to ensure that they work together correctly. Integration tests are usually performed after the individual units have been tested and are integrated into the larger system.

Why Unit & Integration Testing?

Unit and integration tests are essential parts of the software development process because they help ensure that the code behaves as expected, meets the requirements, and is reliable and maintainable over time. Unit and integration tests are essential parts of the software development process because they help ensure that the code behaves as expected, meets the requirements, and is reliable and maintainable over time.

The Testing
Trophy

According to the testing trophy introduced by Kent C. Dodds, Integration tests provide a high return on investment and are an essential part of a robust testing strategy. They verify large features or entire pages without using a real database, backend or browser. As unit & integration tests cover larger portions of the system, they are more likely to catch issues that other tests or developers may miss, making them an important component of testing. Therefore, investing more in integration tests compared to other kinds of tests can provide a greater overall benefit to the testing process.

Used Technologies

For OpenMRS3 unit and integration tests, the following set of technologies is used.

  • Jest: A fast and user-friendly test runner with simple configuration, mocks and spies, and coverage reports.
  • React Testing Library: A simpler and more user-friendly alternative to Enzyme, with convenient semantic queries, async utilities, and better error messages. Recommended by the React team.
  • Mock Service Worker (MSW): A tool for realistic API mocking with no network dependencies, flexible response customization, isolation of tests, and support for multiple testing frameworks. Easy to use.

Suggested approach

  1. Write tests for rendering
  2. Write tests for user interactions which don’t involve API calls (Ex: Clicking on a menu opens the menu items)
  3. Write tests for interactions which use event handlers with API calls etc. (Ex: Fill out a login form and submit)
    • Here, use MockServiceWorker for mocking the network calls

Best Practices and Things to Consider

  1. Write more integration tests than any other kind of test.
  2. Avoid testing internals.
    1. The tests should not break if you change how you handle the state or rename state fields or methods. 
    2. The test should be written from the user’s perspective
  3. Tests should be deterministic. (Should not fail due to random reasons)
  4. Avoid unnecessary expectations in tests
  5. We recommend this order of priority:
    1. getByRole
    2. getByLabelText
    3. getByPlaceholderText
    4. getByText
    5. getByDisplayValue
    6. getByAltText
    7. getByTitle
    8. getByTestId
  6. Avoid these common mistakes.

Test Naming Conventions

The tests should have meaningful names and should be nested properly:

  1. Root describe must be the same as the component/page/hook/util we're testing
  2. Nested describes must have the when prefix (to indicate specific scenarios)
  3. Description of it must be a use-case sentence with the "should" prefix


Example Test
describe('useAuth', () => {
    describe('when user exists', () => {
      it('should return the user object', () => {
        // ...
      });
      it('should log out user', () => {
        // ...
      });
    });

    describe('when user does not exist', () => {
      it('should return guest user', () => {
        // ...
      });
      it('should destroy session on window close', () => {
        // ...
      });
	});
});


Resources