Java / Intermediate / Lesson 11
Tests, contracts, and deterministic evidence
Test observable behavior with meaningful cases, explicit failure assertions, and controlled time rather than implementation-shaped checks or timing guesses.
On this page
Lesson goals
- Design examples from equivalence classes and boundaries.
- Write a self-contained test harness that fails reliably.
- Control time and external dependencies.
Choose assertions from the promised behavior
A test compares observed behavior with a requirement-derived expectation. It should fail when an important promise breaks, not merely repeat the same arithmetic as the implementation. For a clamp operation, useful cases include below the lower bound, inside the range, and above the upper bound. Name cases so a failure explains the violated contract rather than only displaying two unrelated numbers.
Unit tests isolate a small behavior; integration tests verify boundaries such as filesystem encoding or module packaging; end-to-end tests exercise a wider user path. Choose the narrowest level that can expose the risk, then retain a few important integration paths. Many tests do not compensate for missing the one relevant boundary, and a passing suite does not prove unspecified behavior.
Make a harness fail even without -ea
Java's assert statement is disabled by default and is inappropriate as the only validation of external input. A teaching harness can throw AssertionError explicitly when a check fails, so it works independently of the launcher's assertion setting. The example tests three clamp cases and prints a success line only after all checks complete. A failed check stops normal execution with a nonzero process outcome.
Production test frameworks add reporting, fixtures, parameterized cases, and execution integration, but the core reasoning remains expectation versus observation. Do not swallow AssertionError or catch every Throwable around a whole suite and still print success. Keep setup deterministic and ensure each test can fail for the defect it claims to guard.
public class Main {
static int clamp(int x,int low,int high) {
if(low>high) throw new IllegalArgumentException("range");
return Math.max(low,Math.min(x,high));
}
static void check(boolean ok,String label) { if(!ok) throw new AssertionError(label); }
public static void main(String[] args) throws Exception {
check(clamp(-2,0,10)==0,"below");
check(clamp(5,0,10)==5,"inside");
check(clamp(12,0,10)==10,"above");
System.out.println("3 cases passed");
}
}
3 cases passed
Explicit AssertionError checks remain active without -ea. Each case targets a distinct region of the clamp contract.
Assert the intended failure narrowly
An expected-failure test must fail if no exception occurs and must distinguish the expected exception from unrelated failures. Put the precise operation inside the expectation boundary, not the entire setup. Otherwise a broken fixture could throw the same broad exception and make the test pass without exercising the subject. Check relevant error properties without overfitting to incidental stack-line numbers.
For resource code, inject a failure after acquisition and verify closure. For mutation, inspect the final observable state after rejection to ensure partial changes were not left behind. Avoid tests that merely inspect private field names or repeat each implementation branch mechanically; target invariants, contracts, and boundaries likely to regress during a legitimate refactor.
Control time through a dependency
A method that accepts Clock can be tested at an exact instant without sleeping or changing the system clock. The example fixes UTC time and verifies a deadline that has just been reached. Its contract treats now equal to deadline as expired. Tests immediately before, at, and after the boundary explain the intended equality rule.
Avoid Thread.sleep as synchronization or as a way to wait for business time in a unit test. Scheduling and machine load make such tests slow and flaky. Inject randomness or supply a seeded generator when randomized behavior is part of the requirement, but remember that one seed is one case rather than exhaustive evidence. Keep network and real production files outside isolated tests.
public class Main {
static boolean expired(java.time.Instant deadline,java.time.Clock clock) {
return !java.time.Instant.now(clock).isBefore(deadline);
}
public static void main(String[] args) throws Exception {
var now=java.time.Instant.parse("2026-01-01T00:00:00Z");
var clock=java.time.Clock.fixed(now,java.time.ZoneOffset.UTC);
System.out.println(expired(now,clock));
}
}
true
The comparison treats equality as expired, and the fixed clock makes the result independent of execution date.
Interpret coverage and execution evidence honestly
Coverage indicates executed code locations, not correctness of assertions or completeness of requirements. A test that executes a branch and ignores its result contributes coverage without protecting behavior. Review whether changing the implementation incorrectly would cause a failure. Use property-style checks for broad invariants where useful, such as sorting preserving element counts and yielding ordered output.
Record the compiler release, runtime, test command, and actual outcome. This course validates complete examples in isolated temporary directories and compares exact output; deliberately broken fragments remain non-runnable. Tests should remain readable evidence for the next maintainer, with failures that identify the scenario and requirement. Expand the suite when a new risk or defect warrants it rather than adding redundant cases solely to increase a count.
Common errors and fixes
A test passes with assertions disabled
- Why it happens
- The harness relied only on the assert keyword without -ea.
- How to fix it
- Use a test framework or explicit failing checks, and document launch settings.
- Prevent it next time
- Verify a deliberate wrong result makes the test fail.
A time test fails only on some machines
- Why it happens
- It used wall-clock timing or sleeps instead of a controlled dependency.
- How to fix it
- Inject Clock or use proper synchronization for concurrent work.
- Prevent it next time
- Test exact temporal boundaries deterministically.
Expected-exception test passed for the wrong reason
- Why it happens
- The expectation enclosed unrelated setup or accepted an overly broad exception.
- How to fix it
- Narrow the checked operation and expected type.
- Prevent it next time
- Review failure tests with broken-fixture counterexamples.
Check your understanding
1. An expiry rule uses now >= deadline. Which three time cases belong in a test?
Show worked solution
Immediately before should be false, exactly equal should be true, and immediately after should be true. Use Clock.fixed for each instant, with a documented precision, rather than sleeping across the deadline.
2. A method should reject a negative amount without changing balance. What must the test observe?
Show worked solution
Assert the intended rejection and then verify the balance is unchanged. Checking only that an exception occurred misses a partial mutation before throwing; checking only the balance misses an incorrect silent rejection or wrong failure policy.
Key takeaways
- Assertions should protect requirements rather than private structure.
- Time and failure boundaries deserve deterministic cases.
- Coverage and passing output have limited, explicit meanings.