-
Notifications
You must be signed in to change notification settings - Fork 15
Home
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:
<dependency>
<groupId>de.bechte.junit</groupId>
<artifactId>junit-hierarchicalcontextrunner</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
The version should match your JUnit version, supported since 4.11.