Java Core & JVM Languages

JAX London Blog

JAX London, 09-12 October 2017
The Conference for JAVA & Software Innovation

Nicolai Parlog

JAX London speaker Nicolai Parlog offers a sneak peek into his session and shows you how to write tests with JUnit 5. Read everything you need to know about compatibility with previous JUnit versions, IDEs, and other testing tools.

A small team of dedicated developers is currently working on JUnit 5, the next version of one of Java’s most popular libraries. While the improvements on the surface are deliberately incremental, real innovation happens under the hood, with the potential to redefine testing on the JVM.

The talk will examine those topics in depth and these notes help to follow along.

Setup

All we need to write tests is a small artifact that contains JUnit’s new API:

  • Group ID: org.junit.jupiter
  • Artifact ID: junit-jupiter-api
  • Version: 5.0.0-M2

To actually run tests it is easiest to use IntelliJ 2016.2 or greater,
which has basic native JUnit 5 integration.
If IntelliJ is no option, there are other ways to set JUnit 5 up.

New API

The API to write tests against has undergone a thoughtful evolution.
All of the following snippets are valid JUnit 5 tests – can you spot what’s new?


class JUnit5Test {

	@Test
	void someTest() {
		assertTrue(true);
        }
}

Before And After Methods


@BeforeAll
static void beforeAll() { ... }

@BeforeEach
void beforeEach() { ... }

@AfterEach
void afterEach() { ... }

@AfterAll
static void afterAll() { ... }


Disabling Tests


@Test
@Disabled("Y U No Pass?!")
void failingTest() {
	assertTrue(false);
}



@Test
@DisabledOnFriday
void failingTest() {
	assertTrue(false);
}


Classic Assertions

@Test
void someTest() {
	...
	assertEquals(
		expected,
		actual,
		"Should be equal.");
}



@Test
void someTest() {
	...
	assertEquals(
		expected,
		actual,
		() -> "Should " + "be " + "equal.");
}


Extended Assertions


@Test
void assertAllProperties() {
	Address ad = new Address(
	  "City", "Street", "42");

	assertAll("address",
	  () -> assertEquals("C", ad.city),
	  () -> assertEquals("Str", ad.street),
	  () -> assertEquals("63", ad.number)
	);
}



void methodUnderTest() {
	throw new IllegalStateException();
}

@Test
void assertExceptions() {
	assertThrows(
		Exception.class,
		this::methodUnderTest);
}



@Test
void assertExceptions() {
	Exception ex = expectThrows(
		Exception.class,
		this::methodUnderTest);
	assertEquals("Msg", ex.getMessage());
}


Nesting Tests


class CountTest {
	// lifecycle and tests
	@Nested
	class CountGreaterZero {
		// lifecycle and tests
		@Nested
		class CountMuchGreaterZero {
			// lifecycle and tests
		}
	}
}


Naming Tests


@DisplayName("A count")
class CountTest {
	@Nested
	@DisplayName("when greater zero")
	class CountGreaterZero {
		@Test
		@DisplayName("is positive")
		void isPositive() { ... }
	}
}



@Test
void someTest(MyServer server) {
	// do something with `server`
}

Dynamic Tests

Next thing we’re going to look at are dynamic tests, which make it possible to define tests at run time. To do this we need a @TestFactory -annotated method that returns DynamicTest instances. A DynamicTest is nothing more than a name and a piece of code that can be executed by JUnit.

Say we have a method to test the distance computation of our point class:


void testDistanceComputation(Point p1, Point p2, double expectedDistance) {
	assertEquals(expectedDistance, p1.distanceTo(p2));
}

We can now create a lot of test data and create an individual test for each “point / point / expected distance”-triple.


@TestFactory
Stream<DynamicTest> testDistanceComputations() {
	List<PointPointDistance> testData = createTestData();
	return testData.stream()
		.map(datum -> DynamicTest.dynamicTest(
			"Testing " + datum,
			() -> testDistanceComputation(
				datum.point1(), datum.point2(), datum.distance()
		)));
}

We can also use dynamic tests to define tests with lambda expressions.
By calling upon some uncommon language features and a little hacking we can get as far as this:


class PointTest extends LambdaTest {{

	?(A_Great_Test_For_Point -> {
		// test code
	});

}}

Extension Model

JUnit 5 has a very promising extension model, which defines a list extension points:

  • Test Instance Post Processor
  • BeforeAll Callback
  • Test and Container Execution Condition
  • BeforeEach Callback
  • Parameter Resolution
  • Before Test Execution
  • After Test Execution
  • Exception Handling
  • AfterEach Callback
  • AfterAll Callback

There is an interface for every extension point and we can use annotations to register our implementations of them with JUnit. It will then call them at the appropriate points in the test lifecycle.

Let’s have a look at a simple example where we implement the condition for @DisabledOnFriday:


public class DisabledOnFridayCondition implements TestExecutionCondition {

	@Override
	public ConditionEvaluationResult evaluate(TestExtensionContext context) {
		if (isFriday())
			return ConditionEvaluationResult.disabled("Happy Weekend!");
		else
			return ConditionEvaluationResult.enabled("Fix it!");
	}

	private boolean isFriday() {
		// ...
	}

}

Architecture

Saving the best for last we will have a look at JUnit 5’s architecture.

Splitting JUnit 5

It isolates “JUnit the platform” from “JUnit the tool” by implementing a clear separation of concerns and splitting itself into three subprojects.

JUnit Jupiter

Everything we have seen so far is part of JUnit Jupiter as it defines the new API we write test against in the artifact junit-jupiter-api. But it also contains the means to detect and run those tests. In JUnit 5 this responsibility was given to what’s called engines and accordingly the artifact that contains it is called junit-jupiter-engine.

JUnit Vintage

This is a small subproject that adapts JUnit 3/4 to the engine API so that JUnit 5 can run those tests as well.

JUnit Platform

But JUnit was always more than an API for tests and the means to run them. It doubled as an integration point for various tools and other test frameworks. This is now factored out into JUnit Platform.

Tools can interact with junit-platform-launcher to command JUnit to run tests. The launcher will then find all registered engines and forward the command to them. To that end it also contains junit-platform-engine, which defines the APIs the different engines have to implement. They have the task to detect the tests they are responsible for, parse and run them, and return the results.

The Future Of Testing

Separating JUnit Jupiter and JUnit Platform means that the former can be evolved freely without tools depending on implementation details (which was the case with JUnit 3/4) and that the latter can be used by other test frameworks to gain rich tool support – all they have to do is implement an engine.

Because when evaluating test frameworks it is not only their API that is important. Without proper support by IDEs or build tools it is a non-starter for most serious projects to adopt them. This makes it very hard for new frameworks to establish themselves.

Not so with JUnit 5! All an innovative new framework has to do to gain rich tool support is implement a JUnit engine. This might very well cause a wave of new frameworks and herald the next generation of testing on the JVM!

JAX London talks by Nicolai Parlog:

Top Articles About Java Core & JVM Languages

STAY TUNED!

JOIN OUR NEWSLETTER

Behind the Tracks

Software Architecture & Design
Software innovation & more
Microservices
Architecture structure & more
Agile & Communication
Methodologies & more
DevOps & Continuous Delivery
Delivery Pipelines, Testing & more
Big Data & Machine Learning
Saving, processing & more

JOIN OUR UPCOMING EVENTS IN LONDON!