JUnit·Assertion Playbook
DARK
SE401 · Software Testing and Quality · Chapter 12

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.

For a class Foo → FooTest
6 families of assertions
Pass / Fail / Error
JUnit 5 lifecycle
@Test METHOD assert verdict SET-UP PASS FAIL ERROR TEAR-DN
§ 01 — THE FOUNDATIONS

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.

🔬
Memory hook
"One class, in isolation, by its own author."
Unit testing looks for errors in a subsystem in isolation — usually a single class and its helpers — and it's usually carried out by the developer who wrote it, using black-box and/or white-box techniques to design the cases.
The basic idea
For a given class 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 case — a single test of a single method.
Test suite — a collection of test cases (group your test methods in a class with @SelectPackages or @SelectClasses).
Why JUnit specifically

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.

What JUnit helps the programmer do: define and execute tests and test suites; formalize requirements and clarify architecture; write and debug code; integrate code while always being ready to release a working version. Unit testing is especially important when requirements change frequently — code gets refactored often, and tests confirm the refactor didn't break anything.
A known limitation: JUnit is designed to call methods and compare their return values against expected results. That ignores programs that do work in response to GUI commands, or methods used mainly to produce output. Heavy JUnit use therefore pushes you toward a "functional" style — methods that return a value rather than cause side effects — which is arguably a good thing: simpler, more general, easier to reuse.
§ 02 — ANATOMY OF A TEST

A JUnit test class, piece by piece

The skeleton
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.

The structure of a test method
A test method doesn't return a result.
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.
Naming convention
Class under test: Value
JUnit test for it: ValueTest
Test 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.
Organizing a real project: create test cases in the same package as the code under test. For each Java package in the application, define a 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.
§ 03 — TEST CASE VERDICTS

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.
🌳
Extra memorization aid — decision tree
Two yes/no questions get you to any verdict.
Ask "did it finish?" first, then "was it right?" — never guess between Fail and Error again.
Did the test case finish executing?
No → an unexpected exception or bad set-up occurred → ERROR
Yes — and was the function's behavior as expected?
No, an assertion was false → FAIL
Yes — and all assertions held true?
Yes → PASS
During execution of a test case: if an assertion is true, execution continues. If any assertion is false, execution of the test case stops and it fails. If an unexpected exception is encountered, the verdict is an error. If all assertions were true, the test case passes.
§ 04 — A WORKED EXAMPLE

Reading a real test case, line by line

Straight from the slides — a test for setName() on a class called Value.

A simple JUnit test case — annotated walkthrough
/** 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
}
Reading order that matters on exams: ① a method needs @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.
The class under test — BinarySearch.java
public class BinarySearch {
  public static int search(int[] a, int x) {
        ...
  }

  public static int checkedSearch(int[] a, int x) {
        ...
  }
}
The JUnit test — BinarySearchTest.java
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() { … }
}
Running it in Eclipse/NetBeans: install a JDK, create a Java project whose name matches the sample-code folder, add the 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.
§ 05 — ASSERTION METHODS

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.

🧩
Memory hook
"Trust No Identical Equal Arrays"
True/False · Null · Identity (Same) · Equals · ArrayEquals — plus a Floating-Point variant of Equals. Every family also accepts an optional leading 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);
§ 06 — EXCEPTION TESTING

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.

The method under test — expects to throw 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);
Approach 1 — assertThrows()

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());
}
Approach 2 — try/catch + fail()

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.
§ 07 — TEST FIXTURES

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.

🧅
Extra memorization aid — nesting analogy
"All wraps Each, Each wraps the Test — like an onion."
@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.
Per-test: @BeforeEach / @AfterEach
Set-up — tasks that must be done prior to each test case: create objects to work with, open a network connection, open a file to read/write.

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.
Important: clean-up should not be written at the end of a test case's own body — a failure ends execution of the test case right there, so code after the failing assertion never runs. @AfterEach methods run regardless of the verdict, even if exceptions are thrown or an assertion fails.
Once per class: @BeforeAll / @AfterAll
@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.
Example — using a file as a test fixture
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 }  // ⑤
}
Actual execution order: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.
Once-only set-up / tear-down
@BeforeAll
public static void anyName() {
    // class setup code here
}

@AfterAll
public static void anyName() {
    // class clean up code here
}
§ 08 — TIMED & PARAMETERIZED TESTS

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)
Exam trap: if the question gives you a time in a different unit (e.g. "500 milliseconds"), you need to convert it to the annotation's expected unit — or pass 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));
}
One test method, three separate invocations — one per string in @ValueSource. Each invocation gets its own pass/fail/error verdict, exactly like a hand-written @Test method would.
§ 09 — BEST PRACTICES

Writing tests that fail atomically

"Failure atomically" means: when a test fails, you know exactly what failed — no detective work required.

🚦
Memory hook — TDD cycle
Red → Green → Refactor
1. Write a failing test first (red). 2. Write just enough code to pass it (green). 3. Refactor. 4. Run tests again. 5. Repeat until the software meets its goal. 6. Write new code only when a test is failing.
Structural practices
  • 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.
Writing the assertions themselves
  • 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.
§ 10 — CHEAT SHEET

Every annotation and assertion, in two tables

JUnit 5 annotations
AnnotationRunsTypical use
@TestIdentifies the method as a test caseEvery unit test method
@BeforeEachBefore each testPrepare environment — read input data, init the class
@AfterEachAfter each test, alwaysClean up — delete temp data, restore defaults
@BeforeAll (static)Once, before all tests + all @BeforeEachTime-intensive setup, e.g. connect to a database
@AfterAll (static)Once, after all tests + all @AfterEachClean-up, e.g. disconnect from a database
@Timeout(5)Fails if method exceeds 5 secondsSimple performance testing
@Timeout(value=100, unit=MILLISECONDS)Fails if method exceeds 100 msPerformance testing with explicit unit
@ParameterizedTest + @ValueSourceOnce per supplied valueSame logic, many inputs
@SelectPackages / @SelectClassesGroups tests into a suiteRun a whole group of related tests together
Assertion methods (org.junit.Assert)
FamilyMethod(s)True when…
BooleanassertTrue / assertFalsethe condition matches
NullassertNull / assertNotNullthe reference is/isn't null
IdentityassertSame / assertNotSameexpected == actual (or !=)
EqualityassertEqualsexpected.equals(actual)
ArraysassertArrayEqualssame length + every element equal, recursively
Floating pointassertEquals(exp, act, delta)Math.abs(expected - actual) <= delta
ExceptionsassertThrows(Class, lambda)exactly that exception class is thrown
Unconditionalfail() / fail(message)never — always fails when reached
Chunking table — the six data types every assertion targets
Type of thing you're checkingAsk yourself
A raw boolean expressionIs it true or false?
A reference that might be emptyIs it null, or not?
Two referencesDo they point to the same object?
Two objects' contentAre they .equals() to each other?
Two arraysSame length, every slot equal?
Two decimalsWithin an acceptable delta of each other?
§ 11 — COMMON MISTAKES

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

✗ WRONG"The test threw a NullPointerException it wasn't expecting, so it failed."
✓ RIGHTAn unexpected exception means the test case didn't finish executing — that's an error, not a fail. Fail is reserved for a completed run where an assertion turned out false.

2. assertEquals vs. assertSame

✗ WRONGUsing assertSame(num1, num2) to check that two Integer objects "have the same value."
✓ RIGHTassertSame 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

✗ WRONGassertEquals(100.0, result) on a computed double.
✓ RIGHTFloating-point math accumulates rounding error, so assertEquals on double/float requires a delta parameter: assertEquals(expected, actual, delta).

4. Putting clean-up code at the end of the test method

✗ WRONGWriting file.delete() as the last line inside a @Test method.
✓ RIGHTIf an assertion earlier in that method fails, execution stops immediately — the clean-up line never runs. Put tear-down logic in @AfterEach, which runs regardless of the verdict.

5. Confusing @BeforeEach/@AfterEach with @BeforeAll/@AfterAll

✗ WRONGMarking a database-connection method @BeforeEach and making it non-static, expecting it to run once for the whole class.
✓ RIGHT@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

✗ WRONGA single @Test method with 8 unrelated assertions checking 8 different behaviors.
✓ RIGHTWrite many small tests, each with roughly one assertion at its end — this is what "failure atomically" means: when something breaks, you know exactly what, without reading a stack trace essay.

7. Reaching for assertTrue for everything

✗ WRONGassertTrue(a.equals(b)) instead of a descriptive assertion.
✓ RIGHTassertEquals(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.
§ 12 — POP QUIZ

Quick self-check — click a question to reveal the answer

1. What annotation is required for JUnit to recognize a method as a test case?
@Test. Without it, the method is just a regular method — JUnit won't run it, no matter how it's named.
2. True or False: assertEquals(100.0, 99.99995) (no delta) is the correct way to compare two doubles.
False. Comparing floating-point values needs a third delta argument: assertEquals(expected, actual, delta), true when Math.abs(expected - actual) <= delta.
3. A test method throws a NullPointerException that nothing in the test caught or expected. What's the verdict?
Error — the test case execution was not completed, due to an unexpected exception.
4. Which annotation runs a method once, before any test in the class, and what modifier must that method have?
@BeforeAll, and the method must be static. It runs before any of the tests and before any @BeforeEach methods.
5. What does assertSame(expected, actual) check that assertEquals(expected, actual) does not?
Reference identity — that expected == actual (they are literally the same object in memory), not just that expected.equals(actual) is true.
6. Why is it wrong to put tear-down/clean-up code at the very end of a @Test method body instead of in @AfterEach?
If an earlier assertion in that method fails, execution stops immediately — any code after it, including clean-up, never runs. @AfterEach methods run regardless of the verdict.
7. Which annotation lets one test method run multiple times with different input values, and what supplies those values?
@ParameterizedTest, combined with a source annotation such as @ValueSource that provides the arguments for each invocation.
8. Short answer: following the naming convention from the slides, what would you call the JUnit test class for a class named Order?
OrderTest — class under test X → JUnit test class XTest.
§ 13 — PRACTICE LAB

Diagnose the code, pick the right call

Exam-style scenarios built directly from the slide examples — work out the answer before revealing it.

1. You need to check that 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("Failure: 2 is not in array.", search(a, 2) == -1);
Why: it's a raw Boolean condition (an equality check on an int), so it belongs to the Boolean-condition family — assertTrue with an optional leading message.
2. Two Integer variables, num1 = Integer.valueOf(2013) and num2 = Integer.valueOf(2014). You call assertSame("Should be same.", num1, num2). What happens?
→ Fails, with: "Should be same. expected same:<2013> was not:<2014>"
Why: assertSame checks reference identity (==). Even setting aside that the values differ, two distinct Integer objects are never the same reference outside the small-integer cache — the assertion is false and the test fails (not errors).
3. A method checkedSearch(null, 1) is supposed to throw IllegalArgumentException("Null or empty array."). Write the try/catch version of the test, using fail().
→ try { checkedSearch(null,1); fail("Exception should have occurred"); } catch (IllegalArgumentException e) { assertEquals(e.getMessage(), "Null or empty array."); }
Why: if the exception is thrown, control jumps straight to the catch block and the assertion runs — pass. If it's not thrown, execution reaches fail(), which unconditionally fails the test.
4. A @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?
→ Error
Why: the test case's execution was never completed — it never even got past fixture set-up. That's an error, not a fail (which requires the run to complete with a false assertion) and not a pass.
5. You have int[][] a11 and int[][] a12, both {{2,3},{5,7},{11,13}}. Which single assertion call verifies they're equal, and why not assertEquals?
→ assertArrayEquals("Should be equal", a11, a12);
Why: arrays don't override .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.