Unit Testing Best Practices

Although we all understand the value of unit testing, many of us haven’t had the opportunity to hone our test writing skills to the level of our other code writing. If we are to have confidence that our tests provide the safety net we desire, we need to be as diligent in writing those tests to the best of ability as we are with our production code.

In this presentation, we cover some general best practices for writing unit tests, and provide some examples of dealing with the dreaded legacy code. The theory will be applicable to most programming languages; the specific code examples will be written in Java.

This presentation is directed primarily towards beginner and Intermediate level developers.

In this presentation, you’ll learn about:

  • Common test design pattern
  • Simple unit testing best practices
  • Unit test goals
  • Problematic test examples and how to fix them
  • How to deal with legacy code
  • Test suite best practices

Basic Guidelines

A.A.A.

Common test design pattern is:

  • Arrange
    • Instantiating the class being tested.
    • Setting up required variables, etc.
  • Act
    • Execute the code under test.
  • Assert
    • Confirm the expected result matches the actual.
    • Verify method calls if necessary.

Overall Goals

Tests should be:

  • Maintainable
    • Tests should have minimal duplication.
    • Avoid testing multiple things in a single test.
    • Avoid testing private/protected methods directly. {Will be addressed in a later slide}
  • Trustworthy
    • Does not repeat the logic from the code.
    • Every test should be repeatable 100% of the time.
    • Should never rely on order of execution of the tests.
    • Avoid elements that change such as new Date(), Random, Threads, etc.
    • Avoid using a real database or file system.
  • Readable
    • Follow the A.A.A. pattern as much as possible.
    • Follow proper naming conventions.
    • Proper use of set up and clean up methods.
    • Maintain visibility of values (well named constants can be helpful).

Simple Best Practices

  • A test should only do one thing (Just like a method should!).
    • Regular cases and error cases should not be in the same test.
  • Use blatantly descriptive names.
    • Makes it very easy to see what fails (and why) without looking at the code itself.
  • Refer to the object under test by a specific common name .
    • E.g. fixture or target
  • Elements that are used in all tests should be set up in a test initializer method.
  • Tests should never rely on the order of execution!
  • Tests should not change the global state.
    • If they do (some legacy code may make it so there is no choice) make sure that global state is reverted at the end of the test (potentially in a test clean up method).
  • Try to check for exceptions when they occur instead of generically.
    • Try/catch with Assert.fail() is usually better than @Test(expected = SomeException.class)

Example tests that can be improved

Test #1

What was wrong?

  • The test name tells us nothing.
  • What are we actually testing? The sorter or the case modifier?
  • Are we actually trying to test two things at once?

Test #2

What was wrong?

Test #3

What was wrong?

Test #4

What was wrong?

J.B. Rainsberger states in ‘ JUnit Recipes – Practical Methods for Programmer Testing ’:

“If you want to write a test for a private method, the design may be telling you that the method does something more interesting than merely helping out the rest of the class’s public interface. Whatever that helper method does, it is complex enough to warrant its own test, so perhaps what you really have is a method that belongs to another class – a collaborator of the first.”

“Moreover , by applying this refactoring, you have taken a class that had (at least) two independent responsibilities and split it into two classes, each with its own responsibility. This supports the Single Responsibility Principle of OO programming . ” 1

Or Michael Feathers in ‘ Working Effectively with Legacy Code ’:

“If you have the urge to test a private method, the method shouldn’t be private; if making the method public bothers you, chances are, it is because it is part of a separate responsibility: it should be on another class” 2

What about private methods?

Example:

What we really want is…:

… And:

This class is very testable:

  • Two public methods.
  • No complex dependencies.

As an added bonus, we can now swap in any kind of IFormatter.

For instance, if we want to support different jurisdictions where the date or currency formats are different.

Dealing with Legacy Code

Where to start

Legacy code is often a pseudonym for “Code with minimal testing”.

  • Our assumption is that the code works.
  • Much of the code is very difficult to write tests for (potentially impossible in it’s current state!).
  • Unfortunately we have NO safety net now so any refactoring we do has the potential to break things.
  • Start with any points in the code that are straightforward to add tests for.
  • Follow with the safest (typically the most simple and straightforward) refactor and then add a test for that.
  • Repeat as often as required.

Some Options:

  • Break a large complex method into multiple easily testable methods (or even separate classes).
  • Break a large complex class (with multiple responsibilities) into multiple single responsibility classes.
  • Replace concrete objects with abstract ones.
  • Use polymorphism to isolate units of testable work.
  • Minimize the number of dependencies.
  • Minimize constructor responsibilities (pass in variables rather than instantiate, and do minimal work in the constructor).
Don’t be afraid to step away from a chunk of code that is too difficult to work with for now. You can
always come back to it later when you have built up more of a testing safety net

Example

Option #1 (Extract & Override)

Option #2 (Test Double)

Continued…

Option #2b (Extract Interface):

Continued

Option #3 (Adapter):

Continued…

Option #4 Mocking:

Some more options:

  • Use factories for object instantiation as they are easy to mock/stub (and are a good design pattern regardless).
  • Use adapter classes to communicate with external systems/libraries (as any change to the external system only needs to be handled in a single place in your code).
  • Avoid static methods/classes as much as possible (they are easy to test in their own right, but are difficult to mock when testing other classes).
  • Avoid passing around objects when you only need is a few pieces of data from that object (e.g. don’t pass a whole transaction to a formatter class when all you need to format is the date!)

Test Suite Best Practices

Some basic guidelines

  • Unit tests should run very fast. They should run every time your local code builds.
  • Integration (or full End to End) tests can be significantly slower to run, so those should ideally be set up as manual runs on your local machine (auto on the build server though).
  • Don’t aim for 100% coverage. 70-80% is typically sufficient.
  • Don’t test every single input. Choose 1 or 2 vanilla cases, plus interesting (or edge) cases. Zero or null are often interesting cases.
  • Don’t test the same thing in different tests (duplication in tests is not any better than duplication in code!)
  • Don’t be afraid to drop a test that is excessively slow. You’re better off losing a little coverage than having people stop running tests because they take too long. Discuss this with your team first though, as better solutions may exist.

Continued…

  • Use factories for generating stub objects easily (static classes are fine here).
  • Useful to have generic versions of your objects (where you don’t care about the contents), as well as specific ones where one or more fields have data you are interested in.

Specific examples

If multiple test classes share common configuration steps, or common data sets, then extract out a parent test class. This can be useful for:

  • Instantiating mock/stub objects such as web services.
  • Loading a single common set of test data (potentially into a mock database instance).
  • Maintaining a common set of constants for easy reference