# Designing Code That Is Easy to Test — JUnit 5 & Mockito

Source: https://www.skillbyai.com/en/java-testing/d-testable

> Structure code with dependency injection, pure logic and explicit time and randomness.

## Testability is a design property

Code that is hard to test is usually hard to change too. A few design habits make testing easy. **Inject dependencies** through constructors instead of creating them inside methods with `new` or reaching for singletons; tests can then pass fakes or mocks. **Separate pure logic from side effects**: put calculations (prices, taxes, validation, state transitions) in plain methods or classes that take inputs and return outputs, and keep I/O (database, HTTP, files) in thin outer layers. Pure logic can be tested exhaustively with fast, simple tests. **Make time and randomness explicit**: inject a `Clock`, a random number generator with a seed, or an ID supplier. **Avoid static mutable state** and hidden global configuration. **Keep classes focused** (single responsibility), so each test needs only a few collaborators. **Program to interfaces** at boundaries (repositories, gateways). These are the same principles as good object-oriented design; tests simply make their absence painful sooner.

## Functional core, imperative shell

Pure business logic sits in the middle, easy to test; thin layers around it handle I/O.

![A small solid core circle surrounded by a thinner ring with arrows to external icons such as a database and a cloud.](assets/figures/java-testing/section-6-map.svg) — Figure 6.1 — Pure logic in the core, side effects at the edges.

## Refactoring for testability

The original reaches for globals; the refactored version takes everything it needs.

```java
// hard to test: hidden dependencies, real time, real database
class LateFeeJob {
    void run() {
        var dao = new JdbcLoanDao(DataSourceHolder.get());
        for (Loan l : dao.findOpen()) {
            if (LocalDate.now().isAfter(l.dueDate())) {
                dao.addFee(l.id(), 50 * ChronoUnit.DAYS.between(l.dueDate(), LocalDate.now()));
            }
        }
    }
}

// testable: pure rule + injected collaborators
final class LateFeePolicy {
    long feePaise(LocalDate due, LocalDate today) {
        long daysLate = ChronoUnit.DAYS.between(due, today);
        return daysLate > 0 ? daysLate * 5_000 : 0;          // Rs 50 per day
    }
}

final class LateFeeJob2 {
    private final LoanRepository loans; private final Clock clock; private final LateFeePolicy policy;
    LateFeeJob2(LoanRepository loans, Clock clock, LateFeePolicy policy) { this.loans = loans; this.clock = clock; this.policy = policy; }
    void run() {
        LocalDate today = LocalDate.now(clock);
        for (Loan l : loans.findOpen()) {
            long fee = policy.feePaise(l.dueDate(), today);
            if (fee > 0) loans.addFee(l.id(), fee);
        }
    }
}

@Test
void fiveDaysLateCostsRs250() {
    assertEquals(25_000, new LateFeePolicy().feePaise(LocalDate.of(2026, 10, 1), LocalDate.of(2026, 10, 6)));
}
```

## If a test needs ten mocks, change the code

A long list of mocks in one test is feedback about the class, not about the test. Split responsibilities until each class has a few clear collaborators.

**Quiz:** Why inject a java.time.Clock instead of calling LocalDate.now() directly?

- [ ] Clock is faster
- [ ] LocalDate.now() is deprecated
- [x] Tests can supply a fixed clock, making time-dependent behaviour deterministic
- [ ] Clock stores dates in the database

*Answer:* Tests can supply a fixed clock, making time-dependent behaviour deterministic. An injected Clock makes the current time a controllable dependency.
