Test smells and how to avoid

Conditional test logic

Symptoms

Conditional statements in the test (if, switch). Test code executes differently each time we run it.

Possible solution

We can replace the if statements that steer execution to a call to fail with a Guard Assertion (Xunit) that causes the test to fail before we reach the code we don’t want to execute.

We can replace Conditional Test Logic for verification of complex objects with an Equality Assertion on an Expected Object. If the production code’s equals method is too strict, we can use a Custom Assertion to defi ne test-specific equality.

Hard-to-test code

Symptoms

Some kinds of code are inherently difficult to test—GUI components, multithreaded code, and test code, for example. It may be difficult to get at the code to be tested because it is not visible to a test. It may be problematic to compile a test because the code is too highly coupled to other classes.

Possible solution

The key to testing overly coupled code is to break the coupling (a Test Double or, more specifically, a Test Stub or Mock Object).

The key to testing asynchronous code is to separate the logic from the asynchronous access mechanism.

Test code duplication

Symptoms

The same test code is repeated many times.

Possible solution

The best solution is to use an Extract Method refactoring to create a Test Utility Method from one of the examples and then to generalize that method to handle each of the copies.

Test logic in production

Symptoms

The code that is put into production contains logic that should be exercised only during tests.

The logic in the SUT(System Under Testing) is there solely to support testing. This logic may be “extra stuff” that the tests require to gain access to the SUT’s internal state for fi xture setup or result verifi cation purposes. It may also consist of changes that the logic of the system undergoes when it detects that it is being tested.

Possible solution

Instead of adding test logic into the production code directly, we can move logic into a substitutable dependency. We can put code that should be run in only production into a Strategy [GOF] object that is installed by default and replaced by a Null Object when running our tests.

When a test requires test-specific equality, we should use a Custom Assertion instead of modifying the equals method.

Obscure tests

Symptoms

We are having trouble understanding what behavior a test is verifying.

Possible solution

Use meaningfull namings, use Parameterized Creation Methods to get test objects, fixture values that do not matter to the test should be defaulted within Creation Methods.

Erratic tests

Symptoms

We have one or more tests that run but give different results depending on when they are run and who is running them.

Possible solution

In case of Interacting Tests we could eliminate this problem entirely by using a Fresh Fixture (each test constructs its own brand-new test fixture for its own private use).

In case of Resource Leakage we should convert the test to use a Fresh Fixture by creating the resource as part of the test’s fixture setup phase. This approach ensures that the resource exists wherever it is run.

Slow Tests

Symptoms

The tests take long enough to run that developers don’t run them every time they make a change to the SUT.

Possible solution

We can make our tests run much faster by replacing the slow components with a Test Double that provides near-instantaneous responses.

Reduce the amount of fi xture setup performed by each test.

Avoid asynchronicity in tests by testing the logic synchronously

Assertion Roulette

Symptoms

It is hard to tell which of several assertions within the same test method caused a test failure.

Possible solution

Break up the test into a suite of Single-Condition Tests.

Frequent Debugging

Symptoms

Manual debugging is required to determine the cause of most test failures.

Possible solution

Doing true test-driven development is the best way to avoid the circumstances that lead to Frequent Debugging. We should start as close as possible to the skin of the application and do storytest-driven development—that is, we should write unit tests for individual classes as well as component tests for the collections of related classes to ensure we have good Defect Localization.

Fragile Tests

Symptoms

A test fails to compile or run when the SUT is changed in ways that do not affect the part the test is exercising.

Possible solution

Interface sensitivity: define of a Higher-Level Language that is used to express the tests. The verbs in the test language are translated into the appropriate method calls by the encapsulation layer, which is then the only software that needs to be modifi ed when the interface is altered in somewhat backward-compatible ways.

Behavioral sencitivity: newly incorrect assumptions about the behavior of the SUT used during fixture setup may be encapsulated behind Creation Methods. Similarly, assumptions about the details of post-test state of the SUT can be encapsulated in Custom Assertions or Verifi cation Methods.

The best solution to Data Sensitivity is to make the tests independent of the existing contents of the database—that is, to use a Fresh Fixture.

Context sencitivity: we need to control all the inputs of the SUT if our tests are to be deterministic. If we depend on inputs from other systems, we may need to control these inputs by using a Test Stub.

Last updated