Best Practices for Unit Testing

Introduction

Unit tests verify a small, singular part of the codebase at a time such as a function or method. They provide a way to make changes without breaking the existing expected behavior. This can prevent trouble down the road when parts of the codebase become less understood than they used to be.

The unit being tested is often referred to as the System Under Test (SUT) or Unit Under Test (UUT).

Here are some best practices to follow when writing and running unit tests.


Give tests a descriptive name

Tests should be given a self-documenting name, for example test_coupon_applied_to_order. This helps others understand the intent of the test. In PHPUnit, don’t worry if this is a longer name than you might normally give a function.


Use the AAA (Arrange, Act, Assert) pattern

A common approach to writing tests is the Arrange, Act, Assert pattern.

Arrange:

These are the steps that prepare the System Under Test (SUT) for testing. It can include steps such as creating mocks or setting other prerequisite conditions.

The arrangement should be as minimal as possible. In WooCommerce for example, if the order total calculation was being tested, the products in the order don’t need to have unnecessary image thumbnails added.

Act

These are the steps that trigger the subject of the test. For example, this is where you would call a function or simulate the clicking of a button.

Assert

These are the steps that assert, or verify, the output of the action is correct.

It is preferable to limit the amount of assertions per test to help identify the cause of failures more readily. Too many assertions are also a sign the test is a following a happy path dependent on previous outcomes. These tests should be split into smaller tests with a narrower focus.

However it may be desirable to include many assertions when testing the UI in React components.


Isolate tests from each other

A hallmark of good tests is isolation from each other. To create isolated tests, it is preferable to avoid sharing resources between tests no matter how convenient it might be. This helps prevent having one test depend on parts of the project’s state that could be altered by a previous test, which can create unexpected behavior when changes are made to the dependent test or the system it tests.

Note however that PHPUnit tests generally do not need to run “clean up” routines to reset WordPress to a pristine state. The WordPress Test Library automatically rolls back database changes and other items like hooks and option values between tests.


Avoid logic in tests

Logic such as if conditions and for loops adds complexity to the test, and may introduce bugs in the test. Consider using PHPUnit’s data providers and Jest’s each as an alternative to adding logic to tests.


Don’t test external functionality

To prevent fragile tests, avoid testing functionality from outside the unit. For example, if you are testing a function which hooks into a WooCommerce filter, call this function directly instead of triggering it up the stack from related WooCommerce functionality. This will prevent test breakage due to changes in the output of a filter or other factors outside of your control.


Use mocks

Mocking is another way to prevent testing external functionality. Mock objects are objects which appear like the real object but return predefined or “dummy” values.

A classic example is to mock calls to an REST API endpoint. By mocking these, the tests don’t need to be reliant on an external service being available and returning responses correctly. The mock object should be configured to return the expected output to provide a constant reference point. This also has a nice side-effect of speeding up the tests.


Don’t test implementation details

Testing implementation details makes it difficult to refactor code later. For example, if a function uses WordPress transients to cache data, don’t assert the value of the transient. This is an implementation detail and will cause tests to fail later if the function is refactored with a different storage type. A good way to think about what to test is to think of the things you guarantee to consumers, like return values, or side-effects such as sending an email.

To test a side-effect like sending an email, consider using a mock object and asserting that the appropriate method was called.

Another example of an implementation detail that should not be tested is private methods on a class. If these contain complex functionality, consider extracting them to their own class to enable testing.


Test errors and exceptions

Make sure to verify how your SUT handles unexpected values and conditions. Create tests which input bad data and assert that error conditions are handled correctly.


Write testable code

Writing code that’s simple to test might require a change of approach. While not all code needs to be tested, or indeed is even possible without great effort, a large amount can be made testable with the following practices:


Run a small amount of tests during development

As your unit test suite grows, it can become time-consuming running all of the tests during development. To work around this, you can limit running the tests those related to the current changes.

With Jest this is as simple as running:

jest -o

For PHPUnit, you can use group annotations and run tests with the —group paramater. Alternatively, you can limit tests using the —filter paramater and a keyword matching the unit. For example:

npm run test:php -- —filter coupons


Run tests against older versions of system software

Your project probably supports older system requirements than what was used in development. It is a good practice to periodically run the unit test suite in an environment containing older versions of software, such as PHP, MySQL and NodeJS. This can help catch backwards-compatibility issues before users do.

A powerful approach to this is using a CI pipeline. These typically allow the tests to be run against a combination of software versions of your choosing, known as a “matrix”.