Topic 3.6
Testing Your LLD
In one line
A design is only proven when tests show it works and stays easy to change. A few sharp unit tests for the rules and edge cases are what separate 'it compiles' from 'I'd ship this'.
Think of it like this
A car's crash test. You don't trust a car because it looks strong; you trust it because it was tested in the exact situations that go wrong. Tests are crash tests for your rules.
Key ideas
- 01
Test the rules, not the getters: 'the lot rejects a truck when only small spots are free' is a good test; 'getName returns the name' is noise.
- 02
Arrange → Act → Assert: set up the world, do one action, check one outcome. One behaviour per test, with a name that reads like a sentence.
- 03
Always cover the edge cases you listed in requirements: full lot, duplicate entry, lost ticket, zero or negative amounts, boundary times (exactly 30 minutes).
- 04
Inject time. Never call System.currentTimeMillis() inside logic; pass a java.time.Clock so tests can freeze or move time ('the car stayed 2h 01m').
- 05
Fakes over mocks for your own ports: an InMemoryTicketRepository is simpler and more honest than a chain of when(...).thenReturn(...).
- 06
Concurrency test: start N threads at the same moment with a CountDownLatch, all trying to take the last spot, then assert that exactly one succeeded.
- 07
In an interview, even writing 3–4 test names out loud ('shouldRejectEntryWhenFull', 'shouldChargeDailyMaxForLostTicket') shows production thinking.
Java / Spring map
- →
JUnit 5 + AssertJ for readable assertions; Mockito only for third-party boundaries you don't own.
- →
java.time.Clock.fixed(...) and Clock.offset(...) make time-based rules (fines, expiries, pricing) fully testable.
Code & diagrams
Rule tests, a time-based test with an injected Clock, and a real concurrency test.
class ParkingLotTest {
@Test
void rejectsTruckWhenOnlySmallSpotsFree() {
ParkingLot lot = new ParkingLot(List.of(new Spot("S1", SpotSize.SMALL)));
assertThatThrownBy(() -> lot.park(new Vehicle("KA01", VehicleType.TRUCK)))
.isInstanceOf(NoSpotAvailableException.class);
}
@Test
void firstThirtyMinutesAreFree() {
MutableClock clock = new MutableClock(Instant.parse("2026-01-01T10:00:00Z"));
ParkingService svc = new ParkingService(lotWithOneCarSpot(), new HourlyPricing(), clock);
Ticket t = svc.enter(new Vehicle("KA01", VehicleType.CAR));
clock.advance(Duration.ofMinutes(30));
assertThat(svc.exit(t.id()).amount()).isEqualTo(Money.zero());
}
@Test
void onlyOneOfTenGatesGetsTheLastSpot() throws Exception {
ParkingLot lot = new ParkingLot(List.of(new Spot("M1", SpotSize.MEDIUM)));
int gates = 10;
CountDownLatch start = new CountDownLatch(1);
AtomicInteger wins = new AtomicInteger();
ExecutorService pool = Executors.newFixedThreadPool(gates);
for (int i = 0; i < gates; i++) {
int n = i;
pool.submit(() -> {
start.await(); // everyone starts together
try { lot.park(new Vehicle("CAR" + n, VehicleType.CAR)); wins.incrementAndGet(); }
catch (NoSpotAvailableException ignored) {}
return null;
});
}
start.countDown();
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
assertThat(wins.get()).isEqualTo(1); // never two cars in one spot
}
}Explain without notes
Why should business logic take a Clock instead of calling the system time directly?
What exactly does the CountDownLatch do in the concurrency test, and what would the test miss without it?
Practice
Write 5 test names (no bodies) for the Vending Machine and 5 for Splitwise. Cover at least two edge cases each.
Implement the 'last spot' concurrency test against your own Parking Lot and make it pass.
Trade-offs
- ↔
Mocks make tests fast to write but tie them to implementation details; fakes take a little longer to build but survive refactors.
Completion checklist
My designs take a Clock and interfaces, so every rule is testable without Spring or a database.
I can write a concurrency test that proves no double booking.