Writing Nested Unit Tests
After we have finished this lesson, we:
- Understand why we should write nested unit tests.
- Know how we can write nested unit tests.
This lesson assumes that:
- You are familiar with JUnit 4
- You can use custom test runners
- You know how you can run unit tests by using Maven or Gradle
Watch the Lesson
The text version of this lesson is given in the following:
The Problems of Traditional Unit Tests
Before we can talk about the problems of traditional unit tests, we have to define the term traditional unit test. A traditional test class fulfills these conditions:
- It can contain setup methods, teardown methods, and test methods.
- All methods must be added directly to the test class.
- It can use a custom test runner.
- It can use JUnit 4 rules.
A traditional test class might look as follows:
import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; @Category(UnitTest.class) public class TraditionalTest { @BeforeClass public static void beforeAllTestMethods() { } @Before public void beforeEachTestMethod() { } @After public void afterEachTestMethod() { } @AfterClass public static void afterAllTestMethods() { } @Test public void testOne() { } @Test public void testTwo() { } }
The biggest problem of so called traditional unit tests is duplicate code. Even if we would create our test data and write our assertions by following the instructions given in the earlier lessons of this course, we wouldn’t be able to eliminate this problem.
The reason for this is that:
- We have to create test data that is passed as an input to the system under test or returned by the stubbed methods.
- We have to configure the behavior of the test doubles that are used by the system under test.
One solution to this problem is to create our test data and configure our test doubles in the setup method. Even though this solution could work, it creates three new problems:
- Our setup method would be so huge that it would be very hard to read and maintain.
- Our test methods would be very hard to read, write, and maintain because we cannot “see” the test data and we cannot know how the test doubles are configured.
- if we have to replace the dependencies of the system under test with test doubles, we might have to create one test class per test case because different test cases might require configuration that is not compatible with the other test cases.
In other words, we have to create our test data and configure our test doubles in our test methods. The problem is that if we make changes to the system under test, we might break so many unit tests that it can literally take hours or days to fix them all. This makes no sense.
It’s clear that if we write traditional unit tests, we cannot remove all duplicate code from our test code. Luckily for us, we can solve this problem by writing nested unit tests.
Writing Nested Unit Tests
Getting the Required Dependencies
Before we can write nested unit tests, we have to get the required dependencies. We can do this by declaring the junit-hierarchicalcontextrunner
dependency in our build script.
If we are using Maven, we can declare this dependency by adding the following snippet into our pom.xml file.
<dependency> <groupId>de.bechte.junit</groupId> <artifactId>junit-hierarchicalcontextrunner</artifactId> <version>4.12.1</version> <scope>test</scope> </dependency>
If we are using Gradle, we can declare this dependency by adding the following snippet into our build.gradle file.
testCompile( 'de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1' )
Next, we have to configure the test runner that runs our unit tests.
Configuring the Test Runner
If we want to write nested unit tests, we have to ensure that our unit tests are run by the HierarchicalContextRunner
class. We can do this by annotating our test class with the @RunWith
annotation.
After we have configured the used test runner, the source code of our class looks as follows:
import de.bechte.junit.runners.context.HierarchicalContextRunner; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @RunWith(HierarchicalContextRunner.class) @Category(UnitTest.class) public class NestedTest { }
Let’s find out how we can write nested unit tests.
Writing Nested Unit Tests
Let’s create a simple test class and add a few inner classes into that class. The idea of this exercise is to demonstrate the invocation order of setup, teardown, and test methods. We can create our test class by following these steps:
First, we have to create a test class and add all setup and teardown methods into the created class. After we have added the setup and teardown methods into the created class, we have to add one test method into the test class.
The source code of our test class looks as follows:
import de.bechte.junit.runners.context.HierarchicalContextRunner; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @RunWith(HierarchicalContextRunner.class) @Category(UnitTest.class) public class NestedTest { @BeforeClass public static void beforeAllTestMethods() { System.out.println("Invoked once before all test methods"); } @Before public void beforeEachTestMethod() { System.out.println("Invoked before each test method"); } @After public void afterEachTestMethod() { System.out.println("Invoked after each test method"); } @AfterClass public static void afterAllTestMethods() { System.out.println("Invoked once after all test methods"); } @Test public void rootClassTest() { System.out.println("Root class test"); } }
Second, we have to add a new inner class called ContextA
into the NestedTest
class. After we have created the ContextA
class, we have to add one setup, teardown, and test method into the created inner class.
After we have added this inner class into the NestedTest
class, the source code of our test class looks as follows:
import de.bechte.junit.runners.context.HierarchicalContextRunner; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @RunWith(HierarchicalContextRunner.class) @Category(UnitTest.class) public class NestedTest { @BeforeClass public static void beforeAllTestMethods() { System.out.println("Invoked once before all test methods"); } @Before public void beforeEachTestMethod() { System.out.println("Invoked before each test method"); } @After public void afterEachTestMethod() { System.out.println("Invoked after each test method"); } @AfterClass public static void afterAllTestMethods() { System.out.println("Invoked once after all test methods"); } @Test public void rootClassTest() { System.out.println("Root class test"); } public class ContextA { @Before public void beforeEachTestMethodOfContextA() { System.out.println("Invoked before each test method of context A"); } @After public void afterEachTestMethodOfContextA() { System.out.println("Invoked after each test method of context A"); } @Test public void contextATest() { System.out.println("Context A test"); } } }
Third, we have to add a new inner class called ContextC
into the ContextA
class. After we have created the ContextC
class, we have to add one setup, teardown, and test method into the created inner class.
After we have added this inner class into the ContextA
class, the source code of our test class looks as follows:
import de.bechte.junit.runners.context.HierarchicalContextRunner; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @RunWith(HierarchicalContextRunner.class) @Category(UnitTest.class) public class NestedTest { @BeforeClass public static void beforeAllTestMethods() { System.out.println("Invoked once before all test methods"); } @Before public void beforeEachTestMethod() { System.out.println("Invoked before each test method"); } @After public void afterEachTestMethod() { System.out.println("Invoked after each test method"); } @AfterClass public static void afterAllTestMethods() { System.out.println("Invoked once after all test methods"); } @Test public void rootClassTest() { System.out.println("Root class test"); } public class ContextA { @Before public void beforeEachTestMethodOfContextA() { System.out.println("Invoked before each test method of context A"); } @After public void afterEachTestMethodOfContextA() { System.out.println("Invoked after each test method of context A"); } @Test public void contextATest() { System.out.println("Context A test"); } public class ContextC { @Before public void beforeEachTestMethodOfContextC() { System.out.println("Invoked before each test method of context C"); } @After public void afterEachTestMethodOfContextC() { System.out.println("Invoked after each test method of context C"); } @Test public void contextCTest() { System.out.println("Context C test"); } } } }
We have now created a test class that contains nested unit tests. When we run our unit tests, we see the following output:
------------------------------------------------------- T E S T S ------------------------------------------------------- Invoked once before all test methods Running com.testwithspring.starter.unittests.NestedTest Invoked before each test method Test on root level Invoked after each test method Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.002 sec - in com.testwithspring.starter.unittests.NestedTest Running com.testwithspring.starter.unittests.NestedTest$ContextA Invoked before each test method Invoked before each test method of context A Context A test Invoked after each test method of context A Invoked after each test method Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec - in com.testwithspring.starter.unittests.NestedTest$ContextA Running com.testwithspring.starter.unittests.NestedTest$ContextA$ContextC Invoked before each test method Invoked before each test method of context A Invoked before each test method of context C Context C test Invoked after each test method of context C Invoked after each test method of context A Invoked after each test method Invoked once after all test methods Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec - in com.testwithspring.starter.unittests.NestedTest$ContextA$ContextC
This output reveals the invocation order of the setup, teardown, and test methods. Let’s go through our test methods one by one.
First, when JUnit runs the rootClassTest()
method, it will invoke these methods in the following order:
beforeAllTestMethods()
beforeEachTestMethod()
rootClassTest()
afterEachTestMethod()
afterAllTestMethods()
Second, when JUnit runs the contextATest()
method, it will invoke these methods in the following order:
beforeAllTestMethods()
beforeEachTestMethod()
beforeEachTestMethodOfContextA()
contextATest()
afterEachTestMethodOfContextA()
afterEachTestMethod()
afterAllTestMethods()
Third, when JUnit runs the contextCTest()
method, it will invoke these methods in the following order:
beforeAllTestMethods()
beforeEachTestMethod()
beforeEachTestMethodOfContextA()
beforeEachTestMethodOfContextC()
contextCTest()
afterEachTestMethodOfContextC()
afterEachTestMethodOfContextA()
afterEachTestMethod()
afterAllTestMethods()
In other words, JUnit invokes the setup and teardown methods by following the context hierarchy of the invoked test method. This means that we can eliminate code code by putting our code to the correct place. We will talk more about this in the next lesson of this topic.
This runner supports JUnit rules as well. I left them out from this example because I didn’t want to confuse you. That being said, you can add rules into nested inner classes and JUnit will invoke them in the correct order.
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- Duplicate code is the biggest problem of so called traditional unit tests.
- If we want to write nested unit tests, we have to run our unit tests by using the
HierarchicalContextRunner
class. - JUnit invokes the setup and teardown methods by following the context hierarchy of the invoked test method.
Previous LessonNext Lesson