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-element
Assert 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 delta
For 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
@Timeout
Useful 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
@ParameterizedTest
Repeat 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.