Optivem Journal

Optivem Journal

Clean Architecture

Fragile Unit Tests in Clean Architecture

So tightly coupled to the domain that they break on every refactor - even when behavior never changes

Valentina Jemuović's avatar
Valentina Jemuović
Jul 31, 2026
∙ Paid

📅 Join me: Clean Architecture for Backend Developers on Wed 26th Aug, 5:00 - 6:30 PM (CEST)

🏆 Register now


How Uncle Bob writes unit tests in Clean Architecture

Years ago, I wrote unit tests in Clean Architecture the way Uncle Bob does. Unit tests target use cases and verify the stateful outcomes on repositories and gateways through test doubles.

In the Clean Coders comparative design series, Uncle Bob shows that he writes unit tests targeting use cases, not targeting the domain. That’s why for each use case class he has a corresponding unit test class - a 1:1 mapping between use cases and unit tests.

You’ll notice that even though he targets use cases and not the domain, the tests are still coupled to the domain - they assert directly on domain entities. That turns out to be a challenge, as we’ll see later in this article.

E.g. for the use case PostDocument here’s the unit test PostDocumentTest:

@Test
public void canPostAnyDocument() throws Exception {
    LocalDateTime now = LocalDateTime.now();
    Document createdDocument = postDocument.post(“username”, “text”);
    Document fetchedDocument = UseCaseContext.repository.getDocument(createdDocument.id);
    assertThat(fetchedDocument.username).isEqualTo(“username”);
    assertThat(fetchedDocument.text).isEqualTo(“text”);
    assertThat(fetchedDocument.id).isEqualTo(createdDocument.id);
    assertThat(fetchedDocument.dateTime).isEqualTo(createdDocument.dateTime);
}

Real-life example: eShop - placing orders

Now let’s illustrate Uncle Bob’s approach to unit tests on a more realistic example - the eShop.

A customer places an order. The PlaceOrder use case takes a SKU and a quantity, looks up the product, checks stock, calculates the total order price, saves the order, notifies the customer, and returns the order number. It reaches the outside world through repository interfaces and gateway interfaces.

The domain holds two entities - Order and Product - plus value objects that own invariants a primitive can’t: Quantity (must be positive), Money (currency and arithmetic), OrderNumber (the format we generate). Those repository and gateway interfaces are part of the domain too - abstractions the use case owns and depends on, implemented out at the infrastructure edge.

Unit testing the PlaceOrder use case

So when we adopt Clean Architecture, we write unit tests against the use case. The test class comes out like this:

class PlaceOrderUnitTest {

    ...

    @BeforeEach
    void setUp() {
        orderRepository = new FakeOrderRepository();
        productGateway = new StubProductGateway();
        orderNumberGateway = new StubOrderNumberGateway();
        notificationGateway = new SpyNotificationGateway();

        placeOrder = new PlaceOrder(
            orderRepository, productGateway, orderNumberGateway, notificationGateway);
    }

    @Test
    void calculatesTheTotal() {
        productGateway.willReturn(new Product(“ABC”, Money.of(20), Quantity.of(10)));
        orderNumberGateway.willReturn(OrderNumber.of(“ORD-1001”));

        var request = new PlaceOrderRequest();
        request.setSku(“ABC”);
        request.setQuantity(4);

        var result = placeOrder.execute(request);

        assertThat(result.isSuccess()).isTrue();
        var addedOrder = orderRepository.getOrder(OrderNumber.of(“ORD-1001”));
        assertThat(addedOrder.getSku()).isEqualTo(“ABC”);
        assertThat(addedOrder.getTotalPrice()).isEqualTo(Money.of(80));
    }

    @Test
    void rejectsAnOrderThatExceedsStock() {
        productGateway.willReturn(new Product(“ABC”, Money.of(20), Quantity.of(10)));

        var request = new PlaceOrderRequest();
        request.setSku(“ABC”);
        request.setQuantity(15);

        var result = placeOrder.execute(request);

        assertThat(result.isSuccess()).isFalse();
        assertThat(orderRepository.isEmpty()).isTrue();
    }

    ...
}

🚫Maintenance nightmare - refactoring the domain breaks unit tests

The tests are green. 100% code coverage, 100% mutation coverage.

But then, there comes a day when we need to refactor the domain. That’s when the nightmare starts...

This post is for paid subscribers

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