A unit test calls a bounded piece of code with controlled inputs and asserts observable results or failures against its contract.
Java JUnit tests: boundary cases and observable contracts
Java 21+. Use a JDK that supports this release.
Select tests from the contract
A price rule subtracts a permitted discount from a nonnegative subtotal. Test zero, a full discount, a normal discount, and rejected negative or excessive discounts. Those cases distinguish policy boundaries rather than mirroring private implementation steps.
The test class contains a small rule so it compiles as one file. In a real project, move the rule into src/main/java and keep this class in src/test/java. Use the tested Maven POM with JUnit Jupiter 5.11.0.
assertThrows verifies the failure type. Inspect the returned exception only when a message or structured field is part of the public contract. A test tied to incidental punctuation becomes noisy without protecting behavior.
Avoid false confidence from a happy path
No test here uses sleeps, a network, a clock, or a production database. That keeps the unit test’s inputs controlled. JDBC and HTTP adapters need separate integration tests to cover their real environment.
A branch count or coverage percentage cannot establish correctness. Ask whether an incorrect implementation could still satisfy the assertions. A price rule returning the subtotal unchanged would fail the normal-discount test.
Use fresh mutable state for each test. Shared state can make results depend on test execution order, concealing a failure when a developer runs only one test.
Test class
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class PriceRuleTest {
static long payable(long subtotal, long discount) {
if (subtotal < 0 || discount < 0 || discount > subtotal) {
throw new IllegalArgumentException("Invalid price inputs");
}
return subtotal - discount;
}
@Test void appliesDiscount() { assertEquals(850, payable(1000, 150)); }
@Test void acceptsZero() { assertEquals(0, payable(0, 0)); }
@Test void acceptsFullDiscount() { assertEquals(0, payable(1000, 1000)); }
@Test void rejectsNegativeSubtotal() {
assertThrows(IllegalArgumentException.class, () -> payable(-1, 0));
}
@Test void rejectsNegativeDiscount() {
assertThrows(IllegalArgumentException.class, () -> payable(100, -1));
}
@Test void rejectsExcessDiscount() {
assertThrows(IllegalArgumentException.class, () -> payable(100, 101));
}
}Cost and design choices
Each test evaluates a constant-time rule. The test runner still has discovery and startup costs. Keep performance benchmarks separate from correctness assertions.
This subtraction cannot overflow because validation restricts both operands to nonnegative values with discount no larger than subtotal. That proof is stronger than a comment claiming long can never overflow.
Common Mistakes
- Do not use Java assert for checks unless the execution deliberately enables assertions.
- Do not make tests depend on order or shared mutable fixtures.
- Do not substitute mocked database success for a real transaction test.
Connect the contracts
Compare the boundary explained in Failure contracts with the assumptions made by this program.
Apply this contract in Spring
Spring MockMvc tests: HTTP behavior without claiming a real network test. These lessons keep framework assembly separate from the Java contract.
Continue with checked tenant security
Continue with Test Spring JWT authorization through filters, service proxy and SQL.
Continue with checked tenant commands
Continue with Spring command rollback test: inspect state after an injected failure.
