What has changed? Is it still backwards compatible? Is it hard to migrate? These are some of the questions I want to answer with this post.
Junit 5 is a bundle of 3 modules:
- platform, a platform for developing testing frameworks
- jupiter, the new programming model and extension model
- vintage, for running Junit 3 and 4 tests
One of the biggest changes is the fact that it has no 3rd party dependencies anymore(hamcrest). This was an issue in the past as Junit could not keep up with changes that appeared in the newer versions of hamcrest. Another one is obviously Java 8 support.
Good. We have found out that it is backwards compatible. If you are not designing testing frameworks (like most of the developers) the platform does not sound very appealing. But it you do, you can find more here.
For a developer who writes tests the new programming model seems more interesting. Some examples will be clearer.
Annotations
@Test
@Disabled("I don't wanna run this one")
void testShouldBeDisabled() {
}
@Ignored was replaced by @Disabled with the same semantics, but with a meaningful name. @DisplayName is just a placeholder for the method’s name. Whenever you want to be more explicit about what you are testing use this.
@BeforeClass and @AfterClass were replaced by @BeforeAll and @AfterAll with the same semantics. @Before and @After were replaced by @BeforeEach and @AfterEach.
@RepeatedTest is there for finding flaky tests.
With Junit 4, @Category was the solution to use if you wanted to group your tests into categories and run them as bundles. This required to use empty classes. With 5 now we have @Tag, which basically does the same thing but instead of classes you can use strings.
@Test
@DisplayName("Items should be sell at the auction")
@Tag("pass")
void testSellItemAtAuction() {
int bid1 = 100;
int bid2 = 200;
int bid3 = 300;
Auction auction = new Auction();
auction.setItems(items);
Item goldPen = auction.getItems().get(0);
auction.sendBidForItem(goldPen, bid1);
auction.sendBidForItem(goldPen, bid2);
auction.sendBidForItem(goldPen, bid3);
assertEquals(auction.getAllBidsForItem(goldPen).size(), 3);
Bid winningBid = auction.sellItem(goldPen);
assertEquals(winningBid.getAmount(), bid3);
}
Another notable annotation is @TestInstance. With it you can control the lifecycle of a test class. By default Junit will create a test class instance for each test method that needs to run. If you change this to
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AuctionTest {}
there will be one instance for all test methods. A benefit of this is the fact you can use non-static methods for @BeforeAll and @AfterAll. This must be used with care as state is kept.
We can define our own annotations with meta-annotations.
How can we proceed if we already have rules in our applications? Well we need another solution with the same end result, but this is dependent of the rule itself.
public class ItemRule implements TestRule {
private List items;
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
//before
buildItems();
base.evaluate();
//after
System.out.println("Items created");
}
};
}
private List buildItems() {
items = new ArrayList<>();
items.add(new Item("code1", 100));
items.add(new Item("code2", 1500));
return items;
}
public List getItems() {
return items;
}
}
Our particular example is pretty straightforward and a @BeforeAll will solve it.
List items = new ArrayList();
@BeforeAll
void buildItems() {
items.add(new Item("code1", 100));
items.add(new Item("code2", 1500));
}
We will get back to this in a minute.
@Nested scope is to build some connections between tests. This example is self-explanatory.
Now that we have looked at some of the changes in the programming model it is time to focus on the extension model. We are still not satisfied with the rule workaround that we suggested above. We need something more elegant. Extensions seem the way to go here.
Now that we have some ideas, we can notice that ParameterResolver is something that we can use. This allows us to control a parameter that can be used in the contructor of the class or in a method’s signature.
public class ItemExtension implements ParameterResolver {
private List buildItems() {
List items = new ArrayList<>();
items.add(new Item("code1", 100));
items.add(new Item("code2", 1500));
return items;
}
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType().equals(List.class) || parameterContext.getParameter().getName().equals("items");
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return buildItems();
}
}
We register it with @ExtendsWith and start using it
@ExtendWith(ItemExtension.class)
class AuctionTest {
List items;
AuctionTest(List items) {
this.items = items;
}
//tests
}
The items are populated as defined in extension.
Extensions solve also the problem that Junit 4 had with Runners. Basically you could have 1 runner per test class, which proved not be enough is some situations. Now you can register as many extensions as you want for a test class.
Assertions
The notifiable changes here are the use of assertions with lambdas.
Assertions can be grouped with assertAll, but this is not new since assertj has this for some time now. Assertions can be nested so if the parent assertion fails, the child assertion will not be executed anymore.
@Test
@DisplayName("This should fail")
@Tag("fail")
void testExpectToFail() {
int bid1 = 100;
int bid2 = 200;
int bid3 = 300;
Auction auction = new Auction();
auction.setItems(items);
Item goldPen = auction.getItems().get(0);
auction.sendBidForItem(goldPen, bid1);
auction.sendBidForItem(goldPen, bid2);
auction.sendBidForItem(goldPen, bid3);
assertAll("bid assertions",
() -> assertEquals(auction.getAllBidsForItem(goldPen).size(), 3, "There are 3 bids for the gold pen"),
() -> {
Bid winningBid = auction.sellItem(goldPen);
assertEquals(winningBid.getAmount(), bid3 + 1, "The winning bid is " + bid3);
});
}
Exceptions testing can be done in many ways (too many). See this post for more information. Since @Rule is not supported anymore, they introduced something similar to assertj (yes, again) and it’s assertThrows
@Test
@DisplayName("Items should NOT be sell at the auction")
@Tag("pass")
void testCannotSellItemAtAuction3() {
int bid1 = 1000;
int bid2 = 800;
Auction auction = new Auction();
auction.setItems(items);
Item goldWatch = auction.getItems().get(1);
auction.sendBidForItem(goldWatch, bid1);
auction.sendBidForItem(goldWatch, bid2);
assertEquals(auction.getAllBidsForItem(goldWatch).size(), 2);
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
auction.sellItem(goldWatch);
});
assertEquals("Cannot sell item " + goldWatch.getCode() + " below the reserve price", exception.getMessage());
}
Another interesting assertion is assertTimeout, which could be useful if you are testing threads. More on thread testing here. Ok, as you can see there were not many changes here and event these changes were not new, they were already in place in libraries like assertj. Even the guys at Junit recognized that these libraries are powerful and did not made any sense to reinvent the wheel.
A nice library that can help you in migrating tests from 4 to 5 is this.
Other features
With dynamic tests you can build and run your tests at runtime. In my experience this has limited use, but I will mention it nevertheless.
An important thing to point out is the surefire plugin issue. Due to this we must use 2.19 version of surefire.
Go to the Junit 5 site for a complete overview.
As a conclusion, Junit 5 looks better and is more powerful. The extension model is a big change as now it is possible to build easily on it. Regarding assertions they remained behind due to the fact that it is a slow moving project and lost the race with other 3rd party frameworks. So I would still recommend assertj for that.