Learn Java with Tests

A TDD course for Java, in the spirit of Learn Go with Tests.

Chapter 5 of 18

05 — Structs (records)

Records as immutable data with behaviour, and float comparisons with a delta.

Kata: Rectangle, Circle, Triangle with perimeter() and area(). New ideas: records (data + behaviour in one declaration), behaviour-driven method naming, floating-point comparison with a delta.

Records: data + behaviour in one line

A record is Java’s way to declare an immutable data type where the compiler gives you the constructor, accessors, equals, hashCode and toString for free:

public record Rectangle(double width, double height) {

    public double perimeter() {
        return 2 * (width + height);
    }

    public double area() {
        return width * height;
    }
}

No boilerplate: width and height become constructor parameters, and the accessors are width() / height() — not getWidth() / getHeight(). Methods that need the data live right next to it.

Step 1 — write the test first

@Test
void calculatesPerimeter() {
    Rectangle rectangle = new Rectangle(10, 5);

    assertEquals(30, rectangle.perimeter(), 0.0001);
}

REDRectangle doesn’t exist. Write a stub, not the working code:

public record Rectangle(double width, double height) {
}

Compiles, perimeter() doesn’t exist → still RED. Good: the test names the method before the compiler has ever seen it.

Step 2 — a subtle Java gotcha: floating-point

Now the honest, important part. assertEquals(10, circle.perimeter(), 0.0001) — see the third argument?

assertEquals(expected, actual, delta);

double arithmetic (0.1 + 0.2 != 0.3 in Java as in every language) means exact equality on floating point is fragile. The delta (0.0001) says “equal within one ten-thousandth”. If you forget it, JUnit 5 has a specific overload: assertEquals(double, double) warns you to add a delta.

Use it on all geometry:

@Test
void calculatesPerimeter() {
    Rectangle rectangle = new Rectangle(10, 5);
    assertEquals(30, rectangle.perimeter(), 0.0001);
}

GREEN.

Step 3 — Circle and Triangle prove the pattern

Circle.area() = π·r², Triangle.area() = Heron’s formula √(s(s−a)(s−b)(s−c)). Same shape: test drives the method, delta compares floats. (Also: Math.sqrt, Math.PI — the Math class is your stdlib maths.)

REFACTOR — parameterize

Three near-identical cases already? Table-test them:

@ParameterizedTest(name = "Rectangle({0} x {1}) perimeter = {2}")
@CsvSource({ "10, 5, 30", "3, 4, 14", "1, 1, 4" })
void calculatesPerimeter(double width, double height, double expected) {
    assertEquals(expected, new Rectangle(width, height).perimeter(), 0.0001);
}

Why this matters for interviews

Records are the default answer to “what’s a plain data holder in modern Java?” (interviewers will drag you toward class Foo { private fields; getters; setters; equals... } — you respond “a record, unless I need mutability”). Immutability by default is exactly what test-driven designs want: no hidden mutable state, no setUp noise, equals is honest.

Run it

gradle test --tests "structs.*"

Next: 06 — interfaces — make the shapes polymorphic through an interface.