Skip to content
Stefan Bechtold edited this page Sep 19, 2013 · 18 revisions

Anyone familiar with Ruby Specs knows, that using a context to structure your unit tests helps to keep your tests clean and readable, while it also reduces the number of boilerplate setup code tremendously.

After watching episode 20 of Uncle Bob’s CleanCoders screencast, I found myself disappointed about the missing context ability of JUnit's default test runner. Also Uncle Bob came up with a workaround of creating contexts by using inner static classes and inheritance (this is all the Enclosed runner supports), I felt like this was wrong and also, that it should be possible to come up with a test runner that supports something like the Ruby Specs Contexts.

The idea was born and I started to work on a hierarchical context runner for JUnit, supporting tests like the following example:

package de.bechte.junit.samples.context;

import de.bechte.junit.runners.context.HierarchicalContextRunner;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertEquals;

@RunWith(HierarchicalContextRunner.class)
public class BankTest {
    private static final double MONEY_DELTA = .00001;

    @BeforeClass
    public static void beforeClassFirstLevel() throws Exception {
        // Silly, just for demonstration, that before class works for the main class
        System.out.println("Setup Database, etc...");
    }

    @AfterClass
    public static void afterClassFirstLevel() throws Exception {
        // Silly, just for demonstration, that after class works for the main class
        System.out.println("Cleanup Database, etc...");
    }

    private static void assertMoneyEquals(double expected, double actual) {
        assertEquals(expected, actual, MONEY_DELTA);
    }

    public class BankContext {
        @Before
        public void setCurrentInterestRate() {
            Bank.currentInterestRate = 2.75;
        }

        @Test
        public void interestRateIsSet() {
            // Rather stupid test, but it shows, that tests
            // on this level get also executed smoothly...
            assertMoneyEquals(2.75, Bank.currentInterestRate);
        }

        public class NewAccountContext {
            private Account newAccount;

            @Before
            public void createNewAccount() throws Exception {
                newAccount = new Account();
            }

            @Test
            public void balanceIsZero() throws Exception {
                assertMoneyEquals(0.0, newAccount.getBalance());
            }

            @Test
            public void interestRateIsSet() throws Exception {
                assertMoneyEquals(2.75, newAccount.getInterestRate());
            }
        }

        public class OldAccountContext {
            private Account oldAccount;

            @Before
            public void createOldAccount() throws Exception {
                oldAccount = new Account();
            }

            public class AfterInterestRateChangeContext {
                private double oldInterestRate = 0;

                @Before
                public void changeInterestRate() {
                    oldInterestRate = Bank.currentInterestRate;
                    Bank.currentInterestRate = 3.25;
                }

                @After
                public void resetInterestRate() {
                    Bank.currentInterestRate = oldInterestRate;
                }

                @Test
                public void shouldHaveOldInterestRate() throws Exception {
                    assertMoneyEquals(2.75, oldAccount.getInterestRate());
                }

                @Test
                public void failingTest() throws Exception {
                    assertMoneyEquals(1.0, Bank.currentInterestRate);
                }

                @Test
                @Ignore
                public void ignoredTest() throws Exception {
                    // whatever
                }
            }
        }
    }
}

Instead of using inner static classes, the hierarchical context runner supports member classes to create a context. This has several advantages over the use of static classes:

  • No inheritance needed
  • The tests within an inner class have access to all members of a higher level class.
  • Static inner classes are not scanned for tests (according to the BlockJUnit4ClassRunner) and can be used to create stubs and helpers

The tricky parts were to support the creation of the test instance and to execute all @Before and @After statements along the context hierarchy for each test.

The test instance needs to be built top down, i.e. create the top level instance, which is required to create the first level instance, which is required to create the second level instance, etc., until the actual test instance can be created. In order to execute all @Before and @After statements defined along the hierarchy of a context another extension needs to be placed to scan all classes along the context hierarchy and gather the statements accordingly.

With these changes in place the runner works great. As it was built upon the BlockJUnit4ClassRunner it also supports all features that are support by this runner. Therefore, no need to hesitate. Check it out today and give it a try. Feedback is very welcomed and appreciated. :)

To use the runner, simply add the following maven dependency to your project or click one of the downloads below:

Maven Dependency

<dependency>
  <groupId>de.bechte.junit</groupId>
  <artifactId>junit-hierarchicalcontextrunner</artifactId>
  <version>4.11</version>
  <scope>test</scope>
</dependency>

Downloads

The version should match your JUnit version, supported since 4.11.

Clone this wiki locally