Testing Basics with JUnit
Write framework-free unit tests with arrange-act-assert, meaningful assertions, and failure-path coverage.
Why it matters
Start from the concept, not the syntax
Meet a concrete situation where this idea earns its place, then state the boundary it guards.
A unit test constructs the object under test directly, controls its collaborators, and proves one observable behavior. Clean arrange-act-assert separation gives failure reports that explain exactly what broke.
By the end of this chapter
Testing private fields or asserting solely on code coverage leads to brittle tests that break during safe refactorings while missing edge-case bugs.
- Arrange-act-assert
- Assertions and failure paths
- Test isolation
Mechanism
How Java expresses the concept
Walk the actual types, rules, and runtime behavior involved—the same behavior described in the Java Language Specification.
The mental model
Arrange the inputs and collaborators, act through one public operation, and assert the result or state change.
Test behavior rather than private implementation details so a safe refactor does not break the test.
Cover normal behavior, invalid input, and important boundaries. Line coverage alone is not confidence.
Keep unit tests isolated; use integration tests for databases, networks, and framework wiring.
Concept map
Inspect the building blocks as a whole
Use the map as a structural reference; it shows the pieces and their relationships, not an execution sequence.
Core Java
Testing behavior with JUnit
A unit test constructs the object under test directly, controls its collaborators, and proves one observable behavior.
- 01Arrange-act-assert
- 02JUnit 5 @Test
- 03Behavior assertions
- 04Failure paths
- 05Test isolation
Implementation
Encode the smallest runnable example
Write and run the code, then observe the behavior you just predicted with a real Java program.
Runnable snippet path
Predict, run, and change each example
Assert success and failure
The test proves both valid behavior and the invariant that rejects invalid input.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
record Price(int cents) {
Price {
if (cents < 0) throw new IllegalArgumentException("negative price");
}
}
class PriceTest {
@Test
void accepts_a_valid_price() {
assertEquals(100, new Price(100).cents());
}
@Test
void rejects_a_negative_price() {
assertThrows(IllegalArgumentException.class, () -> new Price(-1));
}
}Remember
Assert the public contract, not implementation details.
Change one input, predict the result, then compile or test it before continuing.
Keep collaborators isolated
A fake keeps a unit test deterministic without testing a database at the same time.
class TaskServiceTest {
@Test
void completes_a_task() {
var repository = new InMemoryTaskRepository();
var service = new TaskService(repository);
service.complete(42);
assertEquals("COMPLETED", repository.statusOf(42));
}
}Remember
Label tests that use a real repository as integration tests.
Change one input, predict the result, then compile or test it before continuing.
Watch out
Break it on purpose, then repair the right layer
Predict the symptom before running the failure case, and close with the completion checklist.
Deliberate failure
Break
Assert private fields with reflection instead of public contract behavior
Observe
Refactoring internal representation breaks tests even when public behavior is intact
Repair
Test observable behavior and public contracts so unit tests remain robust through refactors
Why it matters
Test observable behavior and public contracts so unit tests remain robust through refactors
Completion checklist
- Structure tests with arrange, act, assert
- Assert both successful results and expected exceptions
- Keep unit tests fast, repeatable, and isolated
Go deeper
Official references
The primary sources behind this chapter. Every claim above stays consistent with them.