Take Your Unit Tests to the Next Level
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.
- Testing
getName()
method.
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 CustomerTest
.
ctor_givenNameIsNull_throwsException()
consists of:
- ctor indicates that we are testing constructor behavior
- givenNameIsNull specifies condition when a null
name
is passed in to the constructor - throwsException indicates that when the condition is met (
name
is 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
name
is passed in to the constructor - setsName indicates that when the condition is met (
name
is non-null), class membername
of our testCustomer
should be set.
getName_returnsName()
consists of:
- getName indicates we are testing behavior of the
getName()
method - returnsName indicates that a valid non-null
name
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).
Given-When-Then
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.
// GIVEN
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 @Before
.
When — this is where the action we are testing is triggered.
// WHEN
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).
// THEN
assertTrue(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).
Living documentation
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.
Conclusion
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! ;)
Visit my Android blog to read about Jetpack Compose and other Android topics