Testing with JUnit
Java Unit Tests
It is important to have assurances that the software we write works as expected. We can do this by running our program and verifying that it works as expected; however, every time we make a change to our code, it would be exhuasting to need to rerun our code and test every possible thing to make sure it all still works. Fortunately, we know how to write software, and we can write software to help us with this repetive task of verifying that our code still works.
Unit tests consist of code that tests small software units, e.g., a method. A popular framework for writing unit tests is the Jupiter testing framework provided in the JUnit platform. Here we will see how to make use of JUnit 5 to generate unit tests.
- JUnit 5 is compatible with Java 8 and above
- JUnit 5 uses annotations to define the structure of the tests
- Some common annotations are:
@Test
-- Identifies a method as a test method@BeforeAll
-- Specifies that the method will run once before all of the tests are run@BeforeEach
-- Specifies that the method will run before each test method is run@AfterAll
-- Specifies that the method will run once after all of the tests have been run@AfterEach
-- Specifies that the method will run after each test method has run@Disable
-- Indicates that the method should be ignored
- We verify expected behavior with assertions
- Assertions are convenience methods provided by JUnit that verify a condition is met. If the condition is not met, the test does not pass
- Some common assertions are:
assertTrue(boolean)
-- test passes iftrue
is passed to the methodassertFalse(boolean)
-- test passes iffalse
is passed to the methodassertNull(Object)
-- test passes ifnull
is passed to the methodassertNotNull(Object)
-- test passes if anything other thannull
is passed to the methodassertEquals(Object, Object)
-- test passes if the two objects passed to the method are "equal"1assertNotEquals(Object, Object)
-- test passes if the two objects passed to the method are not "equal"1assertThrows(Class<T>, Executable)
-- verifies that theExecutable
throws an exception of typeClass<T>
Most of these are pretty straight forward, but the let's take a moment to look
at the last one.
Executable
is a functional interface with one method: void execute()
. We can implement
the interface with a lambda expresion:
Assertions.assertThrows(IndexOutOfBoundsException.class, () -> list.add(-1, null));
If the call to list.add(-1, null)
doesn't throw an IndexOutOfBoundsException
,
the assertion fails.
Configuring IntelliJ
Since the JUnit platform is not part of the Java language, we need to configure IntelliJ to make use of it. IntelliJ makes this pretty easy, but be sure to select JUnit 5 rather than JUnit 4. To install and configure JUnit 5:
- Add the following import to your test class:
import org.junit.jupiter.api.*;
- From the error context menu select Add 'JUnit 5.8.1' to classpath
- Select OK on the pop-up window to download JUnit and install it in your project
Sample Tests[^nothing]
Consider the following test class that actually doesn't test anything but shows how the annotations control the execution of the tests.
import org.junit.jupiter.api.*;
class NothingTest {
private static int count;
@BeforeAll
public static void initialize() {
count = 0;
System.out.println("Starting all tests with a count of " + count++);
}
@Test
public void firstTest() {
System.out.println("Running first test with count of " + count++);
}
@Test
public void secondTest() {
System.out.println("Running second test with count of " + count++);
}
@AfterAll
public static void windDown() {
System.out.println("All test finished with count of " + count++);
}
}
If the test for the class are run, we would expect to output of either:
Starting all tests with a count of 0
Running first test with count of 1
Running second test with count of 2
All test finished with count of 3
or
Starting all tests with a count of 0
Running second test with count of 1
Running first test with count of 2
All test finished with count of 3
It could be either because the order in which tests are run is not defined.
If the following methods are added
@BeforeEach
public void setup() {
System.out.println("Runs before each test with a count of " + count++);
}
@AfterEach
public void teardown() {
System.out.println("Runs after each test with a count of " + count++);
}
then the output looks like this:
Starting all tests with a count of 0
Runs before each test with a count of 1
Running first test with count of 2
Runs after each test with a count of 3
Runs before each test with a count of 4
Running second test with count of 5
Runs after each test with a count of 6
All test finished with count of 7
Here you can see that the method annotated with @BeforeEach
runs before
each test method, and the method annoted with @AfterEach
runs after each
test.
Several overloaded methods for assertEquals()
and assertNotEquals()
are available for primitive types and the boxed primitive types. E.g., assertEquals(int, int)
, assertNotEquals(Integer, Integer)
[^nothing] that actually don't test anything