Should I test that?

Should I test value objects?

Verdict

Yes

Test a value object that checks rules or computes new values with plain unit tests: each rule at its boundary, each operation, and equality between two spellings of the same value.

Why

I test value objects that check rules or compute new values, such as EmailAddress or DateRange, with plain unit tests. Blast radius is users, because a wrong rule lets bad values into customer records. Change frequency is rarely: a value object gains a rule a few times a year. Detectability is eventually, since a missing rule creates a plausible value and nothing crashes. Reversibility is with-effort, because stored bad values need a repair script. Test cost is trivial: with no dependencies, a test is one constructor call and one assertion.

When the decision changes
WhenDecisionWhy
The value object wraps one value with no rules, such as a `CustomerId` record around a UUIDDo not test the wrapper; let the compiler reject a `CustomerId` passed as an `OrderId`Detectability rises to immediately and Reversibility falls to trivial: mixing two ID types fails the build
The aggregate root's tests pass a valid and an invalid value at the boundary of every rule of the value object, and run each of its operations with values that give each a different resultDo not add separate value object tests; keep one case per rule in the aggregate testsDetectability rises to immediately and Reversibility falls to trivial: a broken rule fails the aggregate root's tests in CI
A database CHECK constraint enforces the same rule, such as a length above zero, and the value object only carries the value to that columnTest minimally: one test that the first invalid value is rejectedDetectability rises to same-day and Reversibility falls to trivial: the insert fails and stores nothing
The value object has no rules, and other services read its JSON from a message queueTest minimally: one serialization test that asserts the field namesTest cost rises to moderate: the test needs the service's JSON mapper
The value object is `Money`: it adds amounts in one currency and rounds to centsTest mandatory: rounding at half a cent, a currency mismatch, and a sum of many small amountsBlast radius rises to money and Reversibility to costly: a wrong invoice total ends in refunds
The value object holds a national ID number, and its `toString()` is the only mask before log linesTest mandatory: assert that `toString()` output does not contain the full numberBlast radius rises to safety-or-legal, Detectability to never and Reversibility to impossible: a number in log storage cannot be taken back

What breaks if you don't test

A DateRange that accepts an end before its start stores a booking that no date search returns, and the customer reports it days later. An EmailAddress that keeps the case of its input treats Ana@Example.com and ana@example.com as two values, so the duplicate check passes and one customer gets two accounts.

What you lose if you over-test

A test that reads each field back after construction repeats the constructor and fails only on an intended rename. An equality test on a record or a Kotlin data class checks code the compiler wrote, unless a component compares in its own way.

How to test

Write unit tests without mocks or framework setup:

  1. For each rule, the boundary value that passes and the first that fails: a one-day DateRange passes, an end one day before the start fails.
  2. For each operation, one case per branch: overlaps with ranges that touch, overlap, and stay apart.
  3. For equality, two objects built from different spellings of one value.
  4. For a hand-written equals and hashCode, one call to EqualsVerifier.

The JUnit 5 user guide covers parameterized tests, one row per boundary.

When the answer changes

  • The value object starts to hold an amount of money or personal data.
  • The value object loses its rules and becomes a typed wrapper around one value.
  • Another service or a stored JSON column reads its serialized form.

Code example + Counterexample

The record that says 2.5 and 2.50 differ

The common advice says a Java record generates equals, so equality needs no test. The advice fails for a BigDecimal component:

record Length(BigDecimal meters) {
    Length {
        if (meters.signum() <= 0) throw new IllegalArgumentException("length must be positive");
        meters = meters.stripTrailingZeros(); // the fix; remove it and the first test fails
    }
}

class LengthTest {
    @Test
    void sameLengthWrittenTwoWaysIsEqual() {
        assertEquals(new Length(new BigDecimal("2.5")), new Length(new BigDecimal("2.50")));
    }

    @Test
    void rejectsZero() {
        assertThrows(IllegalArgumentException.class, () -> new Length(BigDecimal.ZERO));
    }
}

The record compares components with their own equals, and the BigDecimal documentation states that BigDecimal.equals requires the same value and scale. A form sends 2.5, a NUMERIC(10,2) column returns 2.50, and a catalog search that matches stock by Length shows a board in stock as sold out. Detectability stays eventually, because the page looks normal. A compact constructor may reassign its parameter, as JEP 395 shows, and the equality test keeps that fix in place.

FAQ

Should I unit test value objects or only the aggregate root?

Unit test a value object directly when it holds rules, because it is cheaper to build than a whole aggregate. Test only through the aggregate root when its tests already cover every rule at its boundary and every operation.

Should I test equals and hashCode of a value object?

Test a hand-written equals and hashCode with EqualsVerifier, which checks the equality contract in one call. For a generated equals, test equality only when a component compares in its own way, such as a BigDecimal or an array.

Should I test value objects written as Java records?

Test the parts of a Java record that you wrote: the checks in the compact constructor and methods that compute new values. Do not test the accessors, or a generated equals over String or LocalDate components.

Should I mock value objects in unit tests?

Do not mock value objects; create real instances, because they have no dependencies. A mock hides a broken rule in the real object from every test that uses it.