ARTICLE
Newly-Introduced JUnit 5 Annotations and Classes
From JUnit in Action, Third Edition by Catalin Tudose
--
This article discusses some of the most important new additions in Junit 5.
__________________________________________________________
Take 37% off JUnit in Action, Third Edition. Just enter fcctudose into the discount box at checkout at manning.com.
__________________________________________________________
The @DisplayName annotation
The @DisplayName
annotation can be used over classes and test methods. It helps the engineers at Test It Inc. to declare their own display name for the annotated test class or test method. Typically, it’s used for test reporting in IDEs and build tools. The string argument of the @DisplayName
annotation may contain spaces, special characters, and even emojis.
Listing 1 demonstrates the usage of the @DisplayName
annotation through the class displayname.DisplayNameTest
. The name to be displayed is usually a full phrase to provide significant information about the purpose of the test.
Listing 1 The usage of the @DisplayName annotation
@DisplayName("Test class showing the @DisplayName annotation.") (1)
class DisplayNameTest {
private SUT systemUnderTest = new SUT();
@Test
@DisplayName("Our system under test says hello.") (2)
void testHello() {
assertEquals("Hello", systemUnderTest.hello());
}
@Test
@DisplayName("") (3)
void testTalking() {
assertEquals("How are you?", systemUnderTest.talk());
}
@Test
void testBye() {
assertEquals("Bye", systemUnderTest.bye());
}
}
The result of the execution of these tests from the IntelliJ IDE looks like in figure 1:
The execution of the example from listing 1 does the following:
- It shows the display name applied to the entire class (1).
- Then, we see that we may apply a usual text display name (2).
- We may also include an emoji (3). The test without an associated display name shows the method name.
The @Disabled annotation
The @Disabled
annotation can be used over classes and test methods. It’s used to signal that the annotated test class or test method is currently disabled and shouldn’t be executed. The programmers at Test It Inc. give reasons for disabling a test, llowing the rest of your team to know exactly why this has been done. If it’s applied on a class, it disables all the methods of the test. The disabled tests are also shown differently when each programmer is running them from the IDE, and the disabling reason is displayed into their console.
The usage of the annotation is demonstrated by the classes disabled.DisabledClassTest
and disabled.DisabledMethodsTest
. Listings 2 and 3 show the code for these classes.
Listing 2 The usage of the @Disabled annotation on a test class
@Disabled("Feature is still under construction.") (1)
class DisabledClassTest {
private SUT systemUnderTest= new SUT("Our system under test");
@Test
void testUsualWork() {
boolean canReceiveUsualWork = systemUnderTest.canReceiveUsualWork();
assertTrue(canReceiveUsualWork);
}
@Test
void testAdditionalWork() {
boolean canReceiveAdditionalWork =
systemUnderTest.canReceiveAdditionalWork();
assertFalse(canReceiveAdditionalWork);
}
}
The whole testing class is disabled, and a reason is provided (1). This is the recommended way to work, providing your colleagues (or maybe, at a later time, even you, the author of the class) immediate understanding about why the test isn’t enabled right now.
Listing 3 The usage of the @Disabled annotation on methods
class DisabledMethodsTest {
private SUT systemUnderTest= new SUT("Our system under test");
@Test
@Disabled (1)
void testUsualWork() {
boolean canReceiveUsualWork = systemUnderTest.canReceiveUsualWork ();
assertTrue(canReceiveUsualWork);
}
@Test
@Disabled("Feature still under construction.") (2)
void testAdditionalWork() {
boolean canReceiveAdditionalWork =
systemUnderTest.canReceiveAdditionalWork ();
assertFalse(canReceiveAdditionalWork);
}
}
You see that:
- The code provides two tests, both of them disabled.
- One of the tests is disabled without a given reason (1).
- The other test is disabled with a reason that may be quickly understood (2) — the recommended way to work.
Nested tests
An inner class means a class that is a member of another class. It can access any private instance variable of the outer class, as it’s effective part of that outer class. The typical use case is when two classes are tightly coupled, and it’s logical to provide direct access from the inner one to all instance variables of the outer one.
Following this tightly coupled idea, nested tests give the test writer more capabilities to express the relationship among several groups of tests. Inner classes may be package private.
Our Test It Inc. company is working with customers. Customers have a gender, a first name and a last name. Sometimes, they may have a middle name and a known date when they have become customers. As some parameters may be or may not be present, the engineers are using the builder pattern to create a customer and to test the correct creation.
Listing 4 demonstrates the usage of the @Nested
annotation into the class NestedTestsTest
. The customer under test is “John Michael Smith” (he has a middle name) and the date when he became a customer is also known.
Listing 4 Nested tests
public class NestedTestsTest { (1)
private static final String FIRST_NAME = "John"; (3)
private static final String LAST_NAME = "Smith"; (3)
@Nested (2)
class BuilderTest { (2)
private String MIDDLE_NAME = "Michael";
@Test (4)
void customerBuilder() throws ParseException { (4)
SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("MM-dd-yyyy");
Date customerDate = simpleDateFormat.parse("04-21-2019");
Customer customer = new Customer.Builder( (5)
Gender.MALE, FIRST_NAME, LAST_NAME) (5)
.withMiddleName(MIDDLE_NAME) (5)
.withBecomeCustomer(customerDate) (5)
.build(); (5)
assertAll(() -> { (6)
assertEquals(Gender.MALE, customer.getGender()); (6)
assertEquals(FIRST_NAME, customer.getFirstName()); (6)
assertEquals(LAST_NAME, customer.getLastName()); (6)
assertEquals(MIDDLE_NAME, customer.getMiddleName()); (6)
assertEquals(customerDate, customer.getBecomeCustomer()); (6)
}); (6)
}
}
}
The main test is NestedTestsTest
(1) and it’s tightly coupled with the nested test BuilderTest
(2).
First, NestedTestsTest
defines the first name and the last name of a customer which is used for all nested tests (3).
The nested test, BuilderTest,
verifies the construction of a Customer
object (4) with the help of the builder pattern (5). The verification of the equality of fields is made at the end of the customerBuilder
test (6).
Tagged tests
Tagged tests represent a replacement for the JUnit 4 Categories. The @Tag
annotation can be used over classes and test methods. Tags can later be used to filter test discovery and execution. Listing 5 presents the CustomerTest
tagged class, which tests the correct creation of the customers from Test It Inc.. A use case may be to group your tests into a few categories, based on the business logic and on those things you are effectively testing. Each tests category has its own particular tag. Then, you decide which tests to run, or alternate between running different categories, depending on the current needs.
Listing 5 The CustomerTest tagged class
@Tag("individual") (1)
public class CustomerTest {
private String CUSTOMER_NAME = "John Smith";
@Test
void testCustomer() {
Customer customer = new Customer(CUSTOMER_NAME);
assertEquals("John Smith", customer.getName());
}
}
The @Tag
annotation is added on the whole CustomerTest
class (1).
Listing 6 The CustomerRepositoryTest tagged class
@Tag("repository") (1)
public class CustomersRepositoryTest {
private String CUSTOMER_NAME = "John Smith";
private CustomersRepository repository = new CustomersRepository();
@Test
void testNonExistence() {
boolean exists = repository.contains("John Smith");
assertFalse(exists);
}
@Test
void testCustomerPersistence() {
repository.persist(new Customer(CUSTOMER_NAME));
assertTrue(repository.contains("John Smith"));
}
}
Similarly, the @Tag
annotation is added on the whole CustomerRepositoryTest
class (1).
Listing 7 The pom.xml configuration file
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.20</version>
<!-- (1)
<configuration> (1)
<properties> (1)
<includeTags>individual</includeTags> (1)
<excludeTags>repository</excludeTags> (1)
</properties> (1)
</configuration> (1)
--> (1)
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
</plugin>
In order to activate the usage of the tags, you’ve a few alternatives. One way to do it’s to work at the level of the pom.xml
configuration file. In listing 7, it’s enough to uncomment the configuration node of the surfire plugin (1) and run mvn clean install
.
Another alternative is from the IntelliJ IDEA IDE: you can activate the usage of the tags by going to Run -> Edit Configurations and choose Tags (JUnit 5) as test kind (figure 2).
Assertions
To perform test validation, you use the assert methods provided by the JUnit Assertions class. As you can see from the previous examples, you have to statically import these methods in your test class. Alternatively, you can import the JUnit Assertions class, depending on your taste for static imports. The following table lists some of the most popular assert methods.
JUnit 5 provides a lot of overloaded assertion methods. In fact, it comes with many of the assertion methods that JUnit 4 has and adds a few that lend themselves well to being used with Java 8 lambdas. All JUnit Jupiter assertions are static methods in the org.junit.jupiter.api.Assertions
class. The assertThat()
method that works with Hamcrest matchers has been removed. Hamcrest is a framework that assists writing software tests in JUnit. It supports creating customized assertion matchers (‘Hamcrest’ is an anagram of ‘matchers’), allowing match rules to be defined declaratively. We’ll discuss by the end of this article about the alternatives to be used.
As previously stated, one of the projects at Test It Inc. is testing a system that may startup, receive usual and additional work, and may close itself. After running some operations, there’s more than one single condition that needs to be verified.
For our demonstration, we’ll also use the lambda expressions introduced by Java 8. Lambda expressions enable to treat functionality as a method argument, or code as data. A lambda expression can be passed around as if it was an object and executed on demand.
We’ll detail a few of the examples which are provided by the assertions
package. Listing 8 shows the usage of some of the overloaded assertAll
methods. The heading parameter allows to recognize the group of assertions within the assertAll()
methods. The failure message of the assertAll()
method may provide detailed information about each and every particular assertion within a group. We also use the @DisplayName
annotation to provide easy to understand information about what the test is looking for. Our purpose is the verification of the same system under test (SUT) which we previously introduced.
Besides the heading failure message of the assertAll
method, we provide the rest of arguments as a collection of executables. This is a convenient and shorter way to assert that all supplied executables don’t throw exceptions.
Listing 8 The usage of the assertAll method
class AssertAllTest {
@Test
@DisplayName("SUT should not be under current verification")
void testSystemNotVerified() {
SUT systemUnderTest = new SUT("Our system under test");
assertAll("SUT not under current verification", (1)
() -> assertEquals("Our system under test", (2)
systemUnderTest.getSystemName()), (2)
() -> assertFalse(systemUnderTest.isVerified()) (3)
);
}
@Test
@DisplayName("SUT should be under current verification")
void testSystemUnderVerification() {
SUT systemUnderTest = new SUT("Our system under test");
systemUnderTest.verify();
assertAll("SUT under current verification", (4)
() -> assertEquals("Our system under test", (5)
systemUnderTest.getSystemName()), (5)
() -> assertTrue(systemUnderTest.isVerified()) (6)
);
}
}
Into the first test, the assertAll
method receives a first message parameter, to be displayed if one of the supplied executables throws an exception (1). Then, it receives one executable to be verified with assertEquals
(2) and one executable to be verified with assertFalse
(3). The assertion conditions are written in a shorter manner, to be read at a glance.
Into the second test, the assertAll
method receives a first message parameter, to be displayed if one of the supplied executables throws an exception (4). Then, it receives one executable to be verified with assertEquals
(5) and one executable to be verified with assertTrue
(6). As with the first test, the assertion conditions are written in a shorter manner, to be read at a glance.
Listing 9 shows the usage of some assertion methods with messages. Thanks to Supplier<String>
, instructions required to create a complex message will not be done in case of success. You can decide to use either lambdas or methods reference for the verification of our system under test (SUT), and they’ll improve the performance.
Listing 9 The usage of some assertion methods with messages
…
@Test
@DisplayName("SUT should be under current verification")
void testSystemUnderVerification() {
systemUnderTest.verify();
assertTrue(systemUnderTest.isVerified(), (1)
() -> "System should be under verification"); (2)
}
@Test
@DisplayName("SUT should not be under current verification")
void testSystemNotUnderVerification() {
assertFalse(systemUnderTest.isVerified(), (3)
() -> "System should not be under verification."); (4)
}
@Test
@DisplayName("SUT should have no current job")
void testNoJob() {
assertNull(systemUnderTest.getCurrentJob(), (5)
() -> "There should be no current job"); (6)
}
…
Into the previous example, we see the following:
>
- A condition is verified with the help of the
assertTrue
method (1), and in case of failure, a message is lazily created (2). - Then, a condition is verified with the help of the
assertFalse
method (3), and in case of failure, a message is lazily created (4). - Then, the existence of an object is verified with the help of the
assertNull
method (5), and in case of failure, a message is lazily created (6).
The advantage of using lambda expressions as arguments of assertion methods is that all of them are lazily created, resulting in performance improvements. If the condition is (1) fulfilled, meaning that the test succeeds, the invocation of the lambda expression at (2) doesn’t take place. This wouldn’t be possible if the test were written old style.
There may be situations when you expect some test to be executed within a given interval. In our example, it’s natural that the user expects the system under test to act at some speed when running the given jobs. JUnit 5 offers an elegant solution for this kind of use cases.
Listing 10 shows the usage of some assertTimeout
and assertTimeoutPreemptively
methods. They represent replacements for the JUnit 4 Timeout Rule. Test It Inc. needs to check that the system under test is performant enough, meaning that it executes its jobs within a given timeout.
Listing 10 The usage of some assertTimeout methods
class AssertTimeoutTest {
private SUT systemUnderTest = new SUT("Our system under test");
@Test
@DisplayName("A job is executed within a timeout")
void testTimeout() throws InterruptedException {
systemUnderTest.addJob(new Job("Job 1"));
assertTimeout(ofMillis(500), () -> systemUnderTest.run(200)); (1)
}
@Test
@DisplayName("A job is executed preemptively within a timeout")
void testTimeoutPreemptively() throws InterruptedException {
systemUnderTest.addJob(new Job("Job 1"));
assertTimeoutPreemptively(ofMillis(500), (2)
() -> systemUnderTest.run(200)); (2)
}
}
assertTimeout
waits until the executable finishes (1). The failure message may look like: execution exceeded timeout of 100 ms by 193 ms
assertTimeoutPreemptively
stops the executable when the time expires (2). The failure message may look like: execution timed out after 100 ms
Also, there are situations when you expect some test to be executed and throw an exception — you may force it to run under inappropriate conditions or receive inappropriate input. In our example, it’s natural that the system under test which tries to run without a job assigned to it throws an exception. JUnit 5 offers an elegant solution for this kind of use cases.
Listing 11 shows the usage of some assertThrows
methods. They represent the replacement for the JUnit 4 ExpectedException Rule and the expected attribute of the @Test
annotation.
All assertions can be made against the returned instance of the Throwable
. This makes tests more readable, as we verify that the system under test throws exceptions when a current job is expected but not found.
Listing 11 The usage of some assertThrows methods
class AssertThrowsTest {
private SUT systemUnderTest = new SUT("Our system under test");
@Test
@DisplayName("An exception is expected")
void testExpectedException() {
assertThrows(NoJobException.class, systemUnderTest::run); (1)
}
@Test
@DisplayName("An exception is caught")
void testCatchException() {
Throwable throwable = assertThrows(NoJobException.class,
() -> systemUnderTest.run(1000)); (2)
assertEquals("No jobs on the execution list!",
throwable.getMessage()); (3)
}
}
Into the previous example, we do the following:
- We verify the condition that the call of the
run
method on thesystemUnderTest
object throws aNoJobException
(1). - Then, we verify that a call to
systemUnderTest.run(1000)
throws aNoJobException
and we keep a reference to the thrown exception into thethrowable
variable (2). - Finally, we check the message kept into the
throwable
exception variable (3).
That’s all for this article.
If you want to learn more about the book, check it out on our browser-based liveBook reader here.
This article was originally published here: https://freecontent.manning.com/newly-introduced-junit-5-annotations-and-classes/