Optivem Journal

Optivem Journal

Test Driven Development

TDD: Unit Tests - Backend (user-side API)

Unit tests define *what* are the outcomes of business logic, not *how* it is implemented.

Valentina Jemuović's avatar
Valentina Jemuović
Dec 12, 2025
∙ Paid

👉 Reserve Your Spot for the 2026 ATDD Accelerator


When practicing TDD in Hexagonal Architecture, unit tests should target use cases - not internal classes, not frameworks, and not implementation details.

In other words: our tests interact with the business logic as an external observer.

Unit Tests Target the User-Side API

Unit tests talk only to the user-side API, which represents the component’s use cases:

  • AddItemtoCart

  • RemoveItemFromCart

  • ViewCartContents

  • CalculateCartTotal

  • FilterCartItems

  • GetProductPrice

The tests know nothing about:

  • Internal classes
    (e.g. Cart, CartItem, PricingCalculator, InventoryChecker)

  • Frameworks
    (e.g. Spring, Hibernate, Express, NestJS)

  • Persistence
    (e.g. SQL queries, ORM mappings, database schemas)

  • Network calls
    (e.g. HTTP requests to inventory or payment services)

  • Technical plumbing
    (e.g. transactions, caching, logging, retries)

This is great because it means that we can do refactoring in a safe way.

As long as the use case behaves the same from the outside, we can:

  • Rename classes
    (e.g. CartService → ShoppingCart)

  • Split logic
    (e.g. extracting pricing rules into a separate domain object)

  • Merge logic
    (e.g. consolidating duplicate validation code)

  • Move code around
    (e.g. shifting logic from a service into a domain entity)

  • Change implementations entirely
    (e.g. in-memory storage → database, sync calls → async events)

…and our tests remain green ✅

Use Case First, Implementation Later

In TDD, we write a unit test for a specific use case (the test describes behavior in business terms). Only then do we implement the use case (classes and code emerge naturally).

We focus purely on:

  • Inputs
    (e.g. customerId, sku, quantity, passed to AddItemToCart())

  • Outputs
    (e.g. cart contains the correct items and quantities, total price is calculated correctly)

  • Observable side effects
    (e.g. an exception is thrown when adding an unavailable product, the cart is persisted via the repository interface)

This:

  • Prevents premature design
    (e.g. you don’t design CartItem or PricingCalculator before knowing what behavior is required)

  • Avoids over-engineering
    (e.g. you don’t implement caching, events, or complex inventory logic before it’s needed)

  • Forces the design to stay aligned with real usage
    (e.g. the system only supports adding/removing items, calculating totals, and viewing the cart exactly as users would do, rather than unnecessary internal implementation details)

Big Picture

The test enters through the same door as a client of the user-side API.

Since we want the test to span the hexagon only, we then have to “mock out“ the server-side API, e.g. using fake adapters.


Real-Life Example: Ordering System

For an e-commerce system, suppose we have a frontend and a backend. We model the backend using Hexagonal Architecture. Let’s see what the user-side API looks like on the backend:

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2025 Valentina Jemuović, Optivem · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture