Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Typo

This page covers some common testing best practices and anti-patterns.

Table of Contents

Best Practices

Use Clear Names

Tests should have names that clearly indicate the behavior and state being tested, and should make it easy to determine what failed without having to look at the test's implementation. For instance, catalogReturnsMetacardIdWhenIngestSucceeds or exceptionThrownWhenInvalidUserNameProvided.

Note

JavaDoc comments should be used when capturing a test's state and behavior in the test method name would make it too long.

JUnit 5 @DisplayName annotation and Spock's descriptive method names can also help address those issues.

Test Positive and Negative Scenarios

Always remember to tests all positive and negative scenarios and ensure that the proper results (error code, exception, message, etc.) are returned.


Note

Some error scenarios are extremely difficult to test using end-to-end or component tests, which makes testing all exception scenarios at the unit test level even more critical.

Keep Related Tests Together

Try to keep tests that relate to a similar behavior together inside the test class. This makes it easier for other people to determine what's already been tested and what needs more testing.

Use Proper Assertions and Validations

When writing a test, always:

  • Assert that the expected value is returned
  • Assert that the object under test is in the expected state
  • Verify that the mocked dependencies were called/not called as expected
  • Verify that all the mocked dependencies that were called were called with the right parameters
  • Have just enough assertions and verifications to prove that the test passes, but no less

Assertions and verifications should be repeated in different tests only when they are required to verify the behavior being validated. In other words, separate tests should never assert or verify the same behavior.

Note

This best practice applies mostly to unit and component tests. Since End to End tests tend to be more complex and time consuming, it sometimes makes sense to verify related behavior in a single tests. This should however be avoided if possible as doing so makes it more difficult to determine what the issue is.

Mock Dependencies

When writing unit or component tests, mock all dependencies that may fail. This allows the tests to guarantee that the unit or component under test behaves as excepted when one of its dependencies fails.

This is especially important in unit tests as these may be the only tests where some of those exception scenarios can be tested.


Note

Simple dependencies (e.g., POJOs) or dependencies that are difficult to mock out (e.g., file system, static utility classes) can be used directly without being mocked, as long as doing so doesn't go against the basic testing best practices.

Clean Up After Yourself

Always make sure that tests clean up after themselves to ensure that a test failure doesn't impact other tests.

Use the test tool's fixture capabilities (i.e., cleanup/tear-down methods) to perform any required cleanup and reset the test class to a known state instead of using finally blocks.

Use Existing Test Tools and Frameworks

Do not re-invent the wheel. Every homegrown mock, test tool or framework needs to itself be tested and maintained. There are tons of good testing frameworks and tools out there, use those instead.

Write Clean and Re-Usable Test Code

Maintain testing code as you would production code. This means keep the test code clean and extract re-usable code when it makes sense to do so.

Tests Before Changes

Before changing, fixing a bug or refactoring existing code, always make sure that tests exist and pass first.

If not, write unit tests for the code that is about to be changed or refactored.

When dealing with legacy code that would be difficult to unit test, consider writing component tests instead. Those should be easier to write while still making it safe to refactor or change the code as needed.

Info

Refactorings automatically performed by an IDE are usually safe and may not require adding tests first. This is especially true for simple structural refactorings that do not affect the logic or flow of the code, such as extract class, extract method, etc. If such refactorings make writing the tests easier, then applying them without existing tests can be considered.

Be Test-Driven

Follow the Try tollow the Test-Driven Development Test CycleCycle when it makes sense and feel comfortable with it.

First, come up with an initial list of end to end, component /service and/or unit tests that will be needed to show that the user story, requirement or improvement works as expected or issue has been fixed.

...

  1. Add a test
  2. Run all tests and see if the new test fails
  3. Write the code
  4. Run the tests until they pass
  5. Refactor the code as needed
  6. Move on to the next test

Be BehaviorBe Behavior-Driven

Test behaviors, not methods or lines of code. This includes exceptional (e.g., error or unexpected) behaviors.

Write each test following the Behavior-Driven Development structure, i.e., Given/When/Then. In other words make sure each test:

  • First provides a clear context in which it will be run (given)
  • Performs the operation to be tested (when)
  • Asserts that the expected outcomes have been met (then)

Use Clear Names

Tests should have names that clearly indicate the behavior being tested. For instance, catalogReturnsMetacardIdWhenIngestSucceeds or errorIsReturnedWhenInvalidUserNameIsProvided.

Positive and Negative Testing

Always remember to tests all positive and negative test scenarios and ensure that the proper results (error code, exception, message, etc.) are returned and provided in the later case.

Warning

Some error scenarios are extremely difficult to test using end-to-end or component/service tests, which makes testing all exception scenarios at the unit test level even more critical.

Assertions and Validations

When writing a test, always:

  • Assert that the expected value is returned
  • Assert that the object under test is in the expected state
  • Verify that all the mocked dependencies were called
  • Verify that all the mocked dependencies were called with the right parameters

Mock Dependencies

Use Proper Clean Up Mechanisms

Smells

Tests Difficult to Name

Too Many Dependencies

...

Smells

Tests Difficult to Name

A test that is difficult to name usually indicates that it is trying to verify multiple behaviors and should be broken up. It may also indicate that the class under test is doing too much and is breaking the Single Responsibility Principle and may need to be refactored.

Too Many Mocks

A test that requires many mocks usually indicates that the class under test is breaking the Single Responsibility Principle or the Law of Demeter and may need to be refactored.

Dependencies Difficult to Mock

Classes that make it difficult to mock their dependencies usually need to be refactored to follow the Dependency Inversion Principle and use dependency injection. For cases where the class to be mocked is instantiated inside the class multiple times, consider creating and injecting a factory class or a Supplier, or use the Factory Method design pattern.

A Lot of Repeated Test Code

See Write Clean and Re-Usable Test Code

Creating Mocks Manually

See Use Existing Test Tools and Frameworks

Test Multiple Behaviors in a Single Test

A test should test one and only one behavior. Having assertions and/or verifications that validate different behaviors makes it difficult to know why a test has failed and doesn't provide a good insight into what is being tested.

Repeated Given, When or Then sections in a BDD test, or difficult to name tests are usually two good indicators that a test is doing too much.

Note

When testing behaviors that build on top of each other (e.g., test step A, then test step B after step A has been done, etc.), consider extracting the common steps into private methods or external support class. This will reduce code duplication while ensuring that each test method verifies a specific behavior.

Anti-Patterns

Sleeps

Sleeps in tests should be avoided as they open the door to timing issues and race conditions, slow tests down and are a major cause of test flakiness.

...

  • Replaced the sleep with an active wait loop (a.k.a., polling), i.e., wait until a condition has been met before moving on
  • Use external synchronization, i.e., use existing class notification mechanism to know it has reached a certain state before continuing
  • Refactor the code under test to eliminate concurrency
  • Use external dependency calls or side-effects as synchronization points in the tests

...

Undoing Setups in Specific Tests

Common test fixtures that are run before all or each tests should remain common to all tests and should not be undone by individual tests. Doing so introduces a lot of confusion when reviewing, changing or maintaining the tests.

Instead, code that is common to many but not all tests should be extracted to one or more private methods or external support class to eliminate duplication.

Tests Need to be Run in a Specific Order

This usually indicates that some tests are not properly cleaning up after themselves or are not properly setting up their test prerequisites.