The goal of this post is not to stress the importance of writing unit tests or Test Driven Development (TDD) process. There are plenty of articles out there that make the case for those and introduce various approaches to unit testing. Instead, I will share my experience writing what I think are effective and self-documenting unit tests. The code examples are in Java and JUnit 4.
Writing unit tests is fun if…
- you follow a consistent methodology which enables you to write tests faster.
- your test code is broken down into manageable small chunks (units of work).
- your test methods use effective, easy-to-follow naming convention and structure.
- you can quantify your progress by generating code coverage reports via tools like JaCoCo.
This article concentrates on the first 3 items on this list.
Let’s test a simple
Customer class that performs its own validation at construction time:
I see 3 potential candidates for testing:
- Testing constructor if null parameter is passed in.
- Testing constructor if non-null parameter is passed in.
Once we test all of the above, the class will be fully tested (100% code coverage). Let’s implement those tests:
Test method names
I recommend that your test method names follow a naming convention of methodNameUnderTest_givenCondition_expectedBehavior where givenCondition is optional. Let’s see how it applies to
ctor_givenNameIsNull_throwsException() consists of:
- ctor indicates that we are testing constructor behavior
- givenNameIsNull specifies condition when a null
nameis passed in to the constructor
- throwsException indicates that when the condition is met (
nameis null), we will throw an exception. Note that
@Test(expected = IllegalArgumentException.class)defines concrete exception we expect.
ctor_givenNameIsValid_setsName() consists of:
- ctr indicates that we are testing constructor behavior
- givenNameIsValid specifies condition when a non-null
nameis passed in to the constructor
- setsName indicates that when the condition is met (
nameis non-null), class member
nameof our test
Customershould be set.
getName_returnsName() consists of:
- getName indicates we are testing behavior of the
- returnsName indicates that a valid non-null
nameshould be returned.
Your test method names should provide a good idea about the functionality of the class under test.
Also, note that each test is only a few lines of code and verifies one piece of functionality only (one unit of code being tested).
Let’s look at the contents of test methods themselves. Notice that all tests follow Given-When-Then style of representing test steps. This makes each individual test a logical, easy-to-understand, consistent sequence of steps.
Let’s describe each of the Given-When-Then steps in more detail:
Given — sets up a certain condition or creates the object(s) required to perform the test prior to executing the core test logic. This is effectively a pre-condition for your test.
String expectedName = "linda";
testSubject = new Customer(expectedName);
Note that Given is not always necessary — sometimes there is no setup is needed for a particular test or common setup is performed before each test inside method annotated with
When — this is where the action we are testing is triggered.
String actualName = testSubject.getName();
Then — this is where we assert that once the test action took place, the state of the application is as expected (our code under test behaves the way we expect).
There are cases when explicit Then is not necessary. For instance, if When step is expected to throw an exception (as shown in the example above).
Applying the recommendations above effectively creates concise, easy-to-read living documentation for your project. When your production code changes, the corresponding tests would change thus updating your project’s documentation. Moreover, this living documentation is an important knowledge resource for your entire Tech team especially for those developers and testers just joining your project.
I hope you see the value of applying these conventions in your unit testing. If this is new to you, it is my belief that once you try to adopt these guidelines, writing tests will suddenly become easier, faster and dare I say fun! Besides resulting in fewer bugs, you will find that future code refactoring is significantly easier as well since now your code is backed by consistent self-documenting tests.
P.S. writing good tests is a very marketable skill to have! ;)