class JUnit5Test {
@Test
void someTest() {
assertTrue(true);
}
}
JUnit 5 is work in progress!
This is based on Milestone 4
(released 1st of April 2017).
GA release is tentatively scheduled for September!
Give feedback!
Basics |
Dynamic Tests |
Parameterized Tests |
Extensions |
Architecture |
Setup & Tools |
class JUnit5Test {
@Test
void someTest() {
assertTrue(true);
}
}
⇝ Package visibility suffices!
@BeforeAll
static void beforeAll() { ... }
@BeforeEach
void beforeEach() { ... }
@AfterEach
void afterEach() { ... }
@AfterAll
static void afterAll() { ... }
⇝ Lifecycle annotations have new names.
@Test
@Disabled("Y U No Pass?!")
void failingTest() {
assertTrue(false);
}
⇝ @Ignore
is now @Disabled
.
@Test
@DisabledOnFriday
void failingTest() {
assertTrue(false);
}
⇝ Convenient Extensibility.
But how?
@Test
void assertAllProperties() {
Address ad = new Address(
"City", "Street", "42");
assertAll("address",
() -> assertEquals("C", ad.city),
() -> assertEquals("Str", ad.street),
() -> assertEquals("63", ad.number)
);
}
⇝ assertAll
gathers results from multiple assertions
Output if assertAll
fails:
org.opentest4j.MultipleFailuresError:
address (3 failures)
expected: <C> but was: <City>
expected: <Str> but was: <Street>
expected: <63> but was: (42)
class CountTest {
// lifecycle and tests
@Nested
class CountGreaterZero {
// lifecycle and tests
@Nested
class CountMuchGreaterZero {
// lifecycle and tests
}
}
}
⇝ @Nested
to organize tests in inner classes
@DisplayName("A count")
class CountTest {
@Nested
@DisplayName("when greater zero")
class CountGreaterZero {
@Test
@DisplayName("is positive")
void isPositive() { ... }
}
}
⇝ @DisplayName
to show a nice name
The effects of @Nested
and @DisplayName
:
@Test
void someTest(MyServer server) {
// do something with `server`
}
⇝ Test has parameters!
But where do they come from?
lifecycle works much like before
many details were improved
@Nested
and @DisplayName
make a nice couple
parameter injection
That's all very nice but how is it
Next Generation Testing?
Basics |
Dynamic Tests |
Parameterized Tests |
Extensions |
Architecture |
Setup & Tools |
Up to now tests were identified
by names
(test…
in JUnit 3 and before)
by annotations
(@Test
in JUnit 4 and 5)
⇝ Tests had to be known at compile time.
So what?!
What if we want to create tests
for a set of parameters
based on non-source files
with lambdas
⇝ We need to define tests at run time.
Allow creation of tests at run time.
tests are wrapped into DynamicTest
-s
methods that create them
are annotated with @TestFactory
These are fully-fledged tests:
integrated into lifecycle (soon)
viewed by tools as separate tests
For more details, check this post.
Basics |
Dynamic Tests |
Parameterized Tests |
Extensions |
Architecture |
Setup & Tools |
Dominant use case for parameterizing tests:
have a test method
define input data
run the method once per input
JUnit 5 has native support for that!
@ParameterizedTest
@ValueSource(strings = { " Hello", "JUnit" })
void testValues(String word) { ... }
declare test method with @ParameterizedTest
define input data with different sources
JUnit does the rest:
Parameterized tests need sources for arguments.
These are included:
@ValueSource
is very simple
but only works for single arguments
@EnumSource
injects some or all enum values
@MethodSource
calls a method to create args
@CsvSource
allows defining args as CSV strings
@CsvFileSource
loads args from CSV file
More than one source can be used per method!
Calling a test with all values of an enum:
@ParameterizedTest
@EnumSource(TimeUnit.class)
void testAllEnumValues(TimeUnit unit) { ... }
Calling a test with a few values of an enum:
@ParameterizedTest
@EnumSource(
value = TimeUnit.class,
names = {"NANOSECONDS", "MICROSECONDS"})
void testSomeEnumValues(TimeUnit unit) { ... }
Call a method to provide arguments:
@ParameterizedTest
@MethodSource(names = "createWords")
void testWords(String word) { ... }
static Stream<String> createWords() {
return Stream.of("Hello", "JUnit");
}
For multiple arguments, return Arguments
:
@ParameterizedTest
@MethodSource(names = "createWordsWithLength")
void testLength(String word, int length) { ... }
static Stream<Arguments> createWordsWithLength() {
return Stream.of(
create("Hello", 5),
create("JUnit 5", 7));
}
Define values as CSV strings:
@ParameterizedTest
@CsvSource({ "(0/0), 0", "(0/1), 1", "(1/1), 1.41" })
void testPointNorm(String point, double norm) { ... }
each string provides args for one execution
strings are split by comma to get individual args
How do strings get converted?
JUnit handles primitives, enums, java.time.*
you can implement ArgumentConverter
and apply with @ConvertWith
:
@ParameterizedTest
@CsvSource({ "(0/0), 0", "(0/1), 1", "(1/1), 1.41" })
void testPointNorm(
@ConvertWith(PointConverter.class) Point point,
double norm) { ... }
This also applies to CSV files:
@ParameterizedTest
@CsvFileSource(resources = "/pointNorms.csv")
void testPointNorm(
@ConvertWith(PointConverter.class) Point point,
double norm) { ... }
You can create your own sources:
implement ArgumentsProvider
static class RandomIntegerProvider
implements ArgumentsProvider {
@Override
public Stream<Arguments> arguments(...) {
return new Random().ints(0, 10)
.mapToObj(ObjectArrayArguments::create)
.limit(3);
}
}
You can create your own sources:
apply with @ArgumentsSource
@ParameterizedTest
@ArgumentsSource(RandomIntegerProvider.class)
void testRandomIntegers(Integer argument) { ... }
JUnit 5 has native support for
one variant of parameterized tests:
declare test method with @ParameterizedTest
define input data with different sources, e.g.
@ValueSource
, @MethodSource
, @CsvSource
,
or self-made sources
Other variants can be implemented via extensions!
That's all very nice but is this already
Next Generation Testing?
Basics |
Dynamic Tests |
Parameterized Tests |
Extensions |
Architecture |
Setup & Tools |
From JUnit 5’s Core Principles:
Prefer extension points over features
Quite literally,
JUnit 5 has Extension Points
Instance Post Processor
Template Invocation
Container and Test Execution Condition
BeforeAll and BeforeEach Callbacks
Parameter Resolution
Before Test Execution
After Test Execution
Exception Handling
AfterEach and AfterAll Callbacks
one interface for each extension point
method arguments capture context
public interface BeforeEachCallback
extends Extension {
void beforeEach(
TestExtensionContext context);
}
an extension can use multiple points
to implement its feature
We want to benchmark our tests!
for each test method
write the elapsed time to console
How?
before test execution: store test launch time
after test execution: print elapsed time
public class BenchmarkExtension implements
BeforeTestExecutionCallback,
AfterTestExecutionCallback {
private long launchTime;
// ...
}
@Override
public void beforeTestExecution(
TestExtensionContext context) {
launchTime = currentTimeMillis();
}
@Override
public void afterTestExecution(
TestExtensionContext context) {
printf("Test '%s' took %d ms.%n",
context.getDisplayName(),
currentTimeMillis() - launchTime);
}
Remember This?
@Test
@DisabledOnFriday
void failingTest() {
assertTrue(false);
}
Let’s see how it works!
public class DisabledOnFridayCondition
implements TestExecutionCondition {
@Override
public ConditionEvaluationResult evaluate(
TestExtensionContext context) {
if (isFriday())
return disabled("Weekend!");
else
return enabled("Fix it!");
}
}
What about parameter injection?
@Test
void someTest(MyServer server) {
// do something with `server`
}
public class MyServerParameterResolver
implements ParameterResolver {
@Override
public boolean supports(ParameterContext p, ...) {
return MyServer.class
== p.getParameter().getType();
}
@Override
public Object resolve(ParameterContext p, ...) {
return new MyServer();
}
}
How do we apply extensions?
@ExtendWith(DisabledOnFridayCondition.class)
class JUnit5Test {
...
}
That’s technical and verbose… :(
Meta-annotations to the rescue!
JUnit 5’s annotations are meta-annotations
JUnit 5 checks recursively for annotations
⇝ We can create our own annotations!
@ExtendWith(DisabledOnFridayCondition.class)
public @interface DisabledOnFriday { }
@Test
@Tag("integration")
@ExtendWith(BenchmarkExtension.class)
@ExtendWith(MyServerParameterResolver.class)
public @interface IntegrationTest { }
@IntegrationTest
@DisabledOnFriday
void testLogin(MyServer server) { ... }
flexibility because of many extension points
extensions compose well
customizable due to meta-annotations
(We left out some details.)
That's all very nice but how is it
Next Generation Testing?
Basics |
Dynamic Tests |
Parameterized Tests |
Extensions |
Architecture |
Setup & Tools |
a single JAR (ignoring Hamcrest)
used by
developers
extensions
IDEs, build-tools
no separation of concerns
tools provide us with awesome features!
but API is not powerful enough
I know, I’ll use reflection!
nothing was safe!
bound tools to implementation details
made maintenance and evolution very hard
Part of JUnit’s success is its great tool support!
But the same tools locked development in.
The success of JUnit as a platform prevents the development of JUnit as a tool.
(Johannes Link)
Separation of concerns:
an API to write tests against
a mechanism to discover and run tests
an API for tools to run tests
Separation of concerns V 2.0:
an API to write tests against
a mechanism to discover and run tests
specific engine per variant of tests
(e.g. JUnit 4 or JUnit 5)
orchestration of engines
API between them
an API for tools to run tests
junit-jupiter-api
junit-jupiter-engine
junit-vintage-engine
junit-platform-engine
junit-platform-runner
a lot more
clear separation of concerns
API for developers
API for tools
That's all very nice but how is it
Next Generation Testing?
Because it opens up the platform!
want to run JUnit 4 tests?
⇝ create an engine for it!
want TestNG to have support like JUnit?
⇝ create an engine for it!
want to write tests in natural language?
⇝ create an engine for it!
Once JUnit 5 adoption sets in:
tools are decoupled from implementation details
tools can support all frameworks (almost) equally well
new frameworks start with full tool support
developers can try out new things
A new generation of test frameworks might arise!
JUnit’s success as a platform
becomes available to everybody.
This heralds the
next generation of testing on the JVM!
Some community engines:
Other projects:
Docker: starts and stops docker containers
Parameterized: "similar to TestNG DataProvider"
JUnit Pioneer: "JUnit 5 Extension Pack"
clear separation of concerns:
APIs for developers, tools,
and new frameworks
opens up the platform
tool support for everybody!
(There’s even more to the story.)
Basics |
Dynamic Tests |
Parameterized Tests |
Extensions |
Architecture |
Setup & Tools |
Add this:
org.junit.jupiter
junit-jupiter-api
5.0.0-M4
Have fun!
JUnit 5 team provides rudimentary
Gradle plugin and Maven Surefire provider
(see user guide for details)
individual classes:
@RunWith(JUnitPlatform.class)
public class JUnit5Test { ... }
all classes:
@RunWith(JUnitPlatform.class)
@SelectPackages({ "my.test.package" })
public class JUnit5TestSuite { }
There is a console launcher:
# run all tests
java -jar junit-platform-console-standalone.jar
--class-path ${path_to_compiled_test_classes}
--scan-class-path
# run a specific test
java -jar junit-platform-console-standalone.jar
--class-path ${path_to_compiled_test_classes}
--select-class ${fully_qualified_test_class_name}
you can start writing tests right away
only IntelliJ has native support
provisional Maven/Gradle integration works
running with JUnit 4 is a good compromise
(Read about the setup details.)
new API is an incremental improvement
full of thoughtful details
dynamic tests are an important concept
wohoo, native parameterized tests!
extension model looks very promising
architecture opens up the platform
tool support is not there yet
(Read more about JUnit 5.)
⇜ Get my book!
I write a mean newsletter,
currently mostly about Java 9.
You can hire me.
architecture diagrams: Nicolai Parlog, CC-BY-NC 4.0