paint-brush
Take Your Unit Tests to the Next Levelby@jshvarts
12,369 reads
12,369 reads

Take Your Unit Tests to the Next Level

by James ShvartsJuly 5th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The goal of this post is not to stress the importance of writing unit tests or <a href="https://hackernoon.com/tagged/test-driven-development" target="_blank">Test Driven Development</a> (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 <em>self-documenting</em> unit tests. The code examples are in <a href="https://hackernoon.com/tagged/java" target="_blank">Java </a>and JUnit 4.

Company Mentioned

Mention Thumbnail
featured image - Take Your Unit Tests to the Next Level
James Shvarts HackerNoon profile picture

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…

  1. you follow a consistent methodology which enables you to write tests faster.
  2. your test code is broken down into manageable small chunks (units of work).
  3. your test methods use effective, easy-to-follow naming convention and structure.
  4. 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:

  1. Testing constructor if null parameter is passed in.
  2. Testing constructor if non-null parameter is passed in.
  3. 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

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:

  1. ctor indicates that we are testing constructor behavior
  2. givenNameIsNull specifies condition when a null name is passed in to the constructor
  3. 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:

  1. ctr indicates that we are testing constructor behavior
  2. givenNameIsValid specifies condition when a non-null name is passed in to the constructor
  3. setsName indicates that when the condition is met (name is non-null), class member name of our testCustomer should be set.

getName_returnsName() consists of:

  1. getName indicates we are testing behavior of the getName() method
  2. 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.



// 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).

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! ;)