Tests & Web Frameworks
A test is code that checks other code. The payoff is not in writing it — it is in changing
code later without guessing what you broke.
JUnit 5
<dependency>
<groupId> org.junit.jupiter</groupId>
<artifactId> junit-jupiter</artifactId>
<version> 5.11.4</version>
<scope> test</scope>
</dependency>
Tests live in src/test/java, mirroring the package structure.
import org.junit.jupiter.api.Test ;
import static org.junit.jupiter.api.Assertions.* ;
class DiscountTest {
@Test
void tenPercentOffOneHundred () {
var discount = new Discount ( 0 . 10 ); // arrange
var result = discount . applyTo ( 10_000 ); // act
assertEquals ( 9_000 , result ); // assert
}
@Test
void negativeDiscountIsRejected () {
assertThrows ( IllegalArgumentException . class , () -> new Discount ( - 0 . 1 ));
}
}
One behaviour per test, and the method name states the rule — not the method being called.
Assertions
assertEquals ( expected , actual );
assertEquals ( 3 . 14 , value , 0 . 001 ); // delta for doubles
assertTrue ( x ); assertFalse ( x );
assertNull ( x ); assertNotNull ( x );
assertArrayEquals ( new int [] { 1 , 2 }, result );
assertThrows ( IllegalArgumentException . class , () -> method ());
assertDoesNotThrow (() -> method ());
assertAll ( "person" ,
() -> assertEquals ( "Ann" , p . name ()),
() -> assertEquals ( 30 , p . age ()));
AssertJ reads better:
assertThat ( result ). isEqualTo ( 9_000 );
assertThat ( names ). hasSize ( 3 ). contains ( "Ann" ). doesNotContain ( "Zoe" );
assertThatThrownBy (() -> new Discount ( - 1 ))
. isInstanceOf ( IllegalArgumentException . class )
. hasMessageContaining ( "negative" );
Lifecycle
@BeforeEach void setUp () { cart = new ShoppingCart (); }
@AfterEach void tearDown () { … }
@BeforeAll static void once () { … }
@AfterAll static void done () { … }
Parameterised tests
@ParameterizedTest
@ValueSource ( ints = { 2 , 4 , 6 , 100 })
void evenNumbers ( int n ) {
assertTrue ( Numbers . isEven ( n ));
}
@ParameterizedTest
@CsvSource ({ "2020, true" , "1900, false" , "2000, true" , "2023, false" })
void leapYears ( int year , boolean expected ) {
assertEquals ( expected , Calendars . isLeapYear ( year ));
}
Other useful annotations: @DisplayName, @Disabled, @Nested, @Tag, @Timeout.
Test doubles
@Test
void orderIsStored () {
var repo = mock ( OrderRepository . class );
when ( repo . nextId ()). thenReturn ( 42L );
new OrderService ( repo ). place ( new Order ( "Ann" , 1000 ));
verify ( repo ). save ( any ( Order . class ));
}
Tip
Mock sparingly — the more you mock, the more you test your assumptions instead of your code.
Pure logic needs no mocks at all.
What to test
edge cases: empty, null, 0, negative, maximum
error paths: is the right exception thrown?
every business rule, one test each
every bug you find: first the failing test, then the fix
Skip getters, setters and generated record code.
Web frameworks
Javalin — minimal
void main () {
var app = Javalin . create (). start ( 7070 );
app . get ( "/hello" , ctx -> ctx . result ( "Hello World" ));
app . get ( "/person/{name}" , ctx -> ctx . json ( new Person ( ctx . pathParam ( "name" ), 30 )));
app . post ( "/person" , ctx -> {
var p = ctx . bodyAsClass ( Person . class );
ctx . status ( 201 ). json ( p );
});
}
Spring Boot — the industry default
@RestController
public class PersonController {
private final PersonRepository repo ;
public PersonController ( PersonRepository repo ) { this . repo = repo ; }
@GetMapping ( "/people" )
public List < Person > all () { return repo . findAll (); }
@GetMapping ( "/people/{id}" )
public Person one ( @PathVariable Long id ) {
return repo . findById ( id ). orElseThrow (() -> new NotFoundException ( id ));
}
@PostMapping ( "/people" )
@ResponseStatus ( HttpStatus . CREATED )
public Person create ( @RequestBody @Valid Person p ) { return repo . save ( p ); }
}
Generate a project skeleton at start.spring.io .
No framework at all
void main () throws Exception {
var server = HttpServer . create ( new InetSocketAddress ( 8080 ), 0 );
server . createContext ( "/hello" , exchange -> {
var body = "Hello World" . getBytes ();
exchange . sendResponseHeaders ( 200 , body . length );
try ( var os = exchange . getResponseBody ()) { os . write ( body ); }
});
server . setExecutor ( Executors . newVirtualThreadPerTaskExecutor ());
server . start ();
}
Where to go next
build something you actually want to use
connect Spring Boot or Quarkus to a database
run your tests in CI (GitHub Actions: mvn verify)
read the JDK sources — they ship with the JDK and are surprisingly readable
See Resources .
★ Exercises
Test isPalindrome from chapter 3 — including empty string, null and punctuation.
Write a parameterised leap-year test with at least six cases.
Write a failing test for a method that does not exist yet, then implement it.
Fully test the ShoppingCart from chapter 11.
Start the built-in HTTP server and call it with curl.
Add a /time endpoint returning the current time as JSON.