Unit testing,
one assertion at a time.
A JUnit test method is basically a tiny courtroom: you set up the facts
(Value v1 = new Value();), call the witness
(v1.setName("Y")), then hand the judge — an assert call — the expected and actual answers. If the judge agrees,
the method does nothing and life goes on. If it disagrees, it throws an error and the
case is closed. This chapter is every rule the judge plays by.
What unit testing is, and why JUnit exists
Get the vocabulary locked in first — "unit," "test case," and "test suite" get used precisely for the rest of the chapter.
Foo, create another class
FooTest to test it, containing various "test case" methods to
run. Each method looks for particular results and passes / fails. JUnit provides
assert commands to help write these checks — put assertion
calls in your test methods for things you expect to be true; if they aren't, the test
fails.
Test suite — a collection of test cases (group your test methods in a class with
@SelectPackages or @SelectClasses).
Faster, higher quality
Write code faster while increasing quality; tests are inexpensive and check their own results with immediate feedback.
Stability
Increases the stability of software — unit tests are developer tests that catch regressions from refactoring.
Free & Java-native
Written in Java, free, elegantly simple, and gives a proper hands-on understanding of unit testing.
A JUnit test class, piece by piece
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; class name { ... @Test void name() { // a test case method ... } }
A method annotated @Test is flagged as a JUnit test case. All @Test methods run automatically when JUnit runs the test class.
If the tests run correctly, the method does nothing.
If a test fails, it throws an
AssertionFailedError.JUnit catches that error and deals with it — you don't have to do anything yourself.
ValueJUnit test for it:
ValueTestTest classes are sometimes placed in a separate package. Each test method represents a single test case and can independently earn a pass, fail, or error verdict.
TestSuite class containing all the tests for that package, plus similar
suites for higher- and lower-level packages. Make sure your build process compiles all tests —
but keep production and test code compiled into separate trees, so you can deploy without
shipping the tests.Pass, Fail, or Error — the only three outcomes
A verdict is the result of executing a single test case. This three-way split is one of the most exam-tested distinctions in the whole chapter.
✓ Pass
- The test case execution was completed.
- The function being tested performed as expected.
- All assertions were true.
✗ Fail
- The test case execution was completed.
- The function being tested did not perform as expected.
- One or more assertions were false — execution of the test case stops right there.
⚠ Error
- The test case execution was not completed.
- Caused by an unexpected event, an unexpected exception, or improper set-up of the test case.
Reading a real test case, line by line
Straight from the slides — a test for setName() on
a class called Value.
/** Test of setName() method, of class Value */ @Test // ① identifies this Java method as a test case public void createAndSetName() { Value v1 = new Value(); v1.setName("Y"); // ② confirm setName saves the given name String expected = "Y"; String actual = v1.getName(); // ③ check the Value object really stored it Assert.assertEquals(expected, actual); // ④ expected vs actual must be equal, or the test fails }
@Test to be recognized as a test case at all — a plain method with no
annotation is just dead code to JUnit. ② the test acts on the object under test. ③ it
observes the resulting state. ④ it asserts that the observed state matches
expectations — this is the only line that can flip the verdict to Fail.public class BinarySearch { public static int search(int[] a, int x) { ... } public static int checkedSearch(int[] a, int x) { ... } }
public class BinarySearchTest { @Test public void testSearch1() { int[] a = { 1, 3, 5, 7 }; assertTrue(search(a, 3) == 1); } @Test public void testSearch2() { int[] a = { 1, 3, 5, 7 }; assertTrue(search(a, 2) == -1); } @Test public void testCheckedSearch1() { … } @Test public void testCheckedSearch2() { … } @Test public void testCheckedSearch3() { … } }
JUnit library
(choose JUnit 5) via Project → Libraries → Add Library, then Run As → JUnit
Test. JUnit 5 requires Java 8+ at runtime (it can still test code compiled with older
JDKs), and is supported by IntelliJ IDEA, Eclipse, NetBeans, VS Code, and build tools Gradle,
Maven, and Ant. JUnit can also run independently from the command line or through a build tool
like Ant — you can use both methods of running JUnit.Six families of assertions, all static in org.junit.Assert
Assertions are Boolean expressions. If the assertion is false, an
AssertionFailedError is thrown. They can check equality of objects
and values, or identity of references — and every one determines the test case verdict.
message argument, printed only on failure.1. Boolean conditions
is it true or false?Assert that a Boolean condition is true or false.
assertTrue(condition) assertFalse(condition) assertTrue(message, condition) assertFalse(message, condition) // examples assertTrue(search(a, 3) == 1); assertFalse("Failure: 2 is not in array.", search(a, 2) >= 0);
2. Null objects
is the reference null?Assert an object reference is null or non-null.
assertNull(object) assertNotNull(object) assertNull(message, object) assertNotNull(message, object) // examples assertNotNull("Should not be null.", new Object()); assertNull("Should be null.", null);
3. Object identity
are they the same reference?Assert two object references are identical.
assertSame(expected, actual) is true if expected == actual; assertNotSame is true
if expected != actual. Order doesn't affect the comparison, but it
does affect the wording of the failure message.
assertSame(expected, actual) assertNotSame(expected, actual) assertSame(message, expected, actual) // examples assertNotSame("Should not be same.", new Object(), new Object()); Integer num1 = Integer.valueOf(2013); assertSame("Should be same.", num1, num1); // passes — same reference Integer num2 = Integer.valueOf(2014); assertSame("Should be same.", num1, num2); // fails! // java.lang.AssertionError: // Should be same. expected same:<2013> was not:<2014>
4. Object equality
is the content equal?Assert two objects are equal. assertEquals(expected,
actual) is true if expected.equals(actual) — it relies
entirely on the class under test's own equals() method, so it's up
to that class to define a suitable one.
assertEquals(expected, actual) assertEquals(message, expected, actual) // examples assertEquals("Should be equal.", "JUnit", "JUnit"); // passes assertEquals("Should be equal.", "JUnit", "Java"); // fails! // org.junit.ComparisonFailure: // Should be equal. expected:<J[Unit]> but was:<J[ava]>
5. Array equality
element-by-elementAssert two arrays are equal. The arrays must have the same length, and each
valid index i is checked recursively — effectively
assertEquals(expected[i], actual[i]) for every position, which
also lets it recurse cleanly into nested (2-D) arrays.
assertArrayEquals(expected, actual) assertArrayEquals(message, expected, actual) // examples int[] a1 = { 2, 3, 5, 7 }; int[] a2 = { 2, 3, 5, 7 }; assertArrayEquals("Should be equal", a1, a2); int[][] a11 = { { 2, 3 }, { 5, 7 }, { 11, 13 } }; int[][] a12 = { { 2, 3 }, { 5, 7 }, { 11, 13 } }; assertArrayEquals("Should be equal", a11, a12);
6. Floating-point values
equal within a deltaFor comparing double or float
values, assertEquals requires an additional
delta parameter, because binary floating-point math is imprecise.
The assertion evaluates to true if Math.abs(expected - actual) <=
delta.
assertEquals(expected, actual, delta) assertEquals(message, expected, actual, delta) // example double d1 = 100.0, d2 = 99.99995; assertEquals("Should be equal within delta.", d1, d2, 0.0001);
A.k.a. robustness testing
Sometimes the correct, expected outcome of a test is an exception — you're proving the code fails safely on bad input.
public static int checkedSearch(int[] a, int x) { if (a == null || a.length == 0) throw new IllegalArgumentException("Null or empty array."); ... } checkedSearch(null, 1);
Specify the expected exception class directly. The verdict is a pass if that exact exception is thrown, and a fail if no exception (or an unexpected one) occurs.
@Test void exceptionTesting() { Exception exception = assertThrows( ArithmeticException.class, () -> calculator.divide(1, 0)); assertEquals("/ by zero", exception.getMessage()); }
Catch the exception manually and call
fail() if it never arrives. This is more verbose, but it lets
you inspect specific messages/details of the exception and distinguish between different
exception types in the same block.
@Test public void testCheckedSearch3() { try { checkedSearch(null, 1); fail("Exception should have occurred"); } catch (IllegalArgumentException e) { assertEquals(e.getMessage(), "Null or empty array."); } }
fail() / fail(message)
is an unconditional failure — it always fails if it's executed. It's used at a point in the code
that should never be reached, such as the line right after a statement that should have thrown
an exception.Set-up, tear-down, and the JUnit lifecycle
A fixture is the context in which a test case executes — the common objects or resources available to any test case, plus the activities that manage them.
@BeforeAll runs once, on the outside, before
anything else. Inside that, @BeforeEach /
@Test / @AfterEach repeat once per
test method. @AfterAll closes the onion on the outside, last of
all.Tear-down — tasks to clean up after execution of each test case: ensures resources are released and the system is in a known state for the next test case.
@AfterEach methods run regardless of the
verdict, even if exceptions are thrown or an assertion fails.
@BeforeAll — on a static method, one method only. Runs
once for the entire test class, before any of the tests and before any
@BeforeEach methods. Useful for starting servers, opening
connections — shared, non-destructive resources with no need to reset per test case.@AfterAll — also on a static method, runs once, after
all tests and after any @AfterEach methods. Useful for stopping
servers, closing connections.
public class OutputTest { private File output; @BeforeEach public void createOutputFile() { // ① and ④ output = new File(...); } @AfterEach public void deleteOutputFile() { // ③ and ⑥ output.close(); output.delete(); } @Test public void test1WithFile() { // code for test case } // ② @Test public void test2WithFile() { // code for test case } // ⑤ }
createOutputFile()
→ ② test1WithFile() → ③ deleteOutputFile() →
④ createOutputFile() → ⑤ test2WithFile() → ⑥
deleteOutputFile(). Notice set-up/tear-down run around each
test. What's not guaranteed is that test1WithFile runs before
test2WithFile — and if multiple methods carry
@BeforeEach, there's no guarantee of the order they run in either.@BeforeAll public static void anyName() { // class setup code here } @AfterAll public static void anyName() { // class clean up code here }
Two JUnit 5 features that scale your test suite
Timed tests
@TimeoutUseful for simple performance tests — network communication, complex
computation. The @Timeout annotation's time unit defaults to
seconds but is configurable. The test fails if the timeout occurs before the test
method completes.
@Test @Timeout(5) // 5 seconds public void testLengthyOperation() { ... } // or with an explicit unit: @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
unit = TimeUnit.MILLISECONDS explicitly instead of assuming
seconds.Parameterized tests
@ParameterizedTestRepeat the same test-case logic multiple times with different data. Declared
just like a regular @Test method but using
@ParameterizedTest instead — and it must declare at least one
source that supplies the arguments for each invocation, which the method then consumes
as a parameter.
@ParameterizedTest @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) void palindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); }
@ValueSource. Each invocation gets its own pass/fail/error verdict,
exactly like a hand-written @Test method would.Writing tests that fail atomically
"Failure atomically" means: when a test fails, you know exactly what failed — no detective work required.
- Separate production and test code — but typically keep them in the same packages.
- Compile into separate trees, allowing deployment without shipping tests.
- Don't forget OO techniques and base classing for shared test logic.
- Each test case should be independent, independent of execution order, with no dependencies on the state left behind by previous tests.
- Give each test a clear, long, descriptive name.
- Assertions should always have clear messages explaining what failed.
- Write many small tests, not one big test — each test should have roughly just 1 assertion at its end.
- Test for expected errors / exceptions explicitly.
- Choose a descriptive assert method — don't force everything through
assertTrue. - Choose representative test cases from equivalent input classes.
- Avoid complex logic in test methods if at all possible.
Every annotation and assertion, in two tables
| Annotation | Runs | Typical use |
|---|---|---|
@Test | Identifies the method as a test case | Every unit test method |
@BeforeEach | Before each test | Prepare environment — read input data, init the class |
@AfterEach | After each test, always | Clean up — delete temp data, restore defaults |
@BeforeAll (static) | Once, before all tests + all @BeforeEach | Time-intensive setup, e.g. connect to a database |
@AfterAll (static) | Once, after all tests + all @AfterEach | Clean-up, e.g. disconnect from a database |
@Timeout(5) | Fails if method exceeds 5 seconds | Simple performance testing |
@Timeout(value=100, unit=MILLISECONDS) | Fails if method exceeds 100 ms | Performance testing with explicit unit |
@ParameterizedTest + @ValueSource | Once per supplied value | Same logic, many inputs |
@SelectPackages / @SelectClasses | Groups tests into a suite | Run a whole group of related tests together |
| Family | Method(s) | True when… |
|---|---|---|
| Boolean | assertTrue / assertFalse | the condition matches |
| Null | assertNull / assertNotNull | the reference is/isn't null |
| Identity | assertSame / assertNotSame | expected == actual (or !=) |
| Equality | assertEquals | expected.equals(actual) |
| Arrays | assertArrayEquals | same length + every element equal, recursively |
| Floating point | assertEquals(exp, act, delta) | Math.abs(expected - actual) <= delta |
| Exceptions | assertThrows(Class, lambda) | exactly that exception class is thrown |
| Unconditional | fail() / fail(message) | never — always fails when reached |
| Type of thing you're checking | Ask yourself |
|---|---|
| A raw boolean expression | Is it true or false? |
| A reference that might be empty | Is it null, or not? |
| Two references | Do they point to the same object? |
| Two objects' content | Are they .equals() to each other? |
| Two arrays | Same length, every slot equal? |
| Two decimals | Within an acceptable delta of each other? |
Where students actually lose points
Seven confusions that show up over and over on quizzes and in real test code.
1. Mixing up Fail vs. Error
2. assertEquals vs. assertSame
assertSame(num1, num2) to check that two Integer objects "have the
same value."assertSame
checks reference identity (==) — two separately-created objects
with equal content will still fail it. Use assertEquals for
content equality via .equals().3. Comparing floats/doubles with plain assertEquals
assertEquals(100.0,
result) on a computed double.assertEquals on double/float requires a delta parameter:
assertEquals(expected, actual, delta).4. Putting clean-up code at the end of the test method
file.delete()
as the last line inside a @Test method.@AfterEach, which runs regardless of the verdict.5. Confusing @BeforeEach/@AfterEach with @BeforeAll/@AfterAll
@BeforeEach and making it non-static, expecting it to run once
for the whole class.@BeforeAll /
@AfterAll run once per class and must be on a
static method. @BeforeEach /
@AfterEach run once per test method and are instance
methods.6. One giant test method with many assertions
@Test
method with 8 unrelated assertions checking 8 different behaviors.7. Reaching for assertTrue for everything
assertTrue(a.equals(b))
instead of a descriptive assertion.assertEquals(a, b)
is more descriptive and produces a much more useful failure message (showing both
expected and actual values) than a generic true/false check.Quick self-check — click a question to reveal the answer
assertEquals(100.0, 99.99995)
(no delta) is the correct way to compare two doubles.
delta argument: assertEquals(expected, actual,
delta), true when Math.abs(expected - actual) <=
delta.NullPointerException
that nothing in the test caught or expected. What's the verdict?
@BeforeEach methods.assertSame(expected, actual) check
that assertEquals(expected, actual) does not?
expected ==
actual (they are literally the same object in memory), not just that
expected.equals(actual) is true.@Test method body instead of in @AfterEach?
@AfterEach methods run regardless of the verdict.@ValueSource that provides the arguments for each
invocation.Order?
X →
JUnit test class XTest.Diagnose the code, pick the right call
Exam-style scenarios built directly from the slide examples — work out the answer before revealing it.
search(a, 2) returns
-1 when 2 isn't in the sorted array {1, 3, 5, 7}, and you want a
custom failure message if it doesn't.assertTrue with an
optional leading message.Integer variables, num1 = Integer.valueOf(2013) and
num2 = Integer.valueOf(2014). You call
assertSame("Should be same.", num1, num2). What happens?checkedSearch(null, 1) is supposed to
throw IllegalArgumentException("Null or empty array."). Write
the try/catch version of the test, using fail().fail(), which unconditionally fails the test.@BeforeEach method calls
output.open(), which throws an unchecked I/O exception because
the disk is full. What's the verdict of the @Test method that
was about to run?int[][] a11 and
int[][] a12, both {{2,3},{5,7},{11,13}}.
Which single assertion call verifies they're equal, and why not assertEquals?.equals() to compare
contents (it falls back to reference identity), so assertEquals
would incorrectly fail two equal-content arrays. assertArrayEquals
checks length and recurses element-by-element — including nested arrays.