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.
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:
getName()
method.Once we test all of the above, the class will be fully tested (100% code coverage). Let’s implement those tests:
g
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 CustomerTest
.
ctor_givenNameIsNull_throwsException()
consists of:
name
is passed in to the constructorname
is null), we will throw an exception. Note that @Test(expected = IllegalArgumentException.class)
defines concrete exception we expect.ctor_givenNameIsValid_setsName()
consists of:
name
is passed in to the constructorname
is non-null), class member name
of our testCustomer
should be set.getName_returnsName()
consists of:
getName()
methodname
should 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.
// GIVENString 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 @Before
.
When — this is where the action we are testing is triggered.
// WHENString 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).
// THENassertTrue(expectedName.equals(actualName));
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! ;)