Testing Use Cases with Test Doubles
Use case orchestration with fakes, stubs and mocks around real domain objects.
You are viewing a free preview lesson.
In the previous lesson, we tested domain logic directly: Product.changePrice(), Order.markPaid(), Order.cancel() and the rest, with no Spring, no PostgreSQL, no HTTP and no mocks. Now we move one layer outward. Our use cases (CreateProductUseCase, UpdateProductUseCase, CreateOrderUseCase, CancelOrderUseCase, PayOrderUseCase) coordinate business operations across boundaries: repositories, the external PaymentService and the Clock. The goal of this lesson is:
Test application orchestration with controlled test doubles while keeping infrastructure out of the test whenever infrastructure behavior is not what we are trying to prove.
What Is a Use Case Test?
A use case test asks questions about the workflow. Did it choose the correct business path and load the authoritative state it needed? Did it reject invalid current state before any side effect, and avoid calling a boundary after an earlier failure? Did it call the correct repository operation, and the external PaymentService only when appropriate? Did it use server-owned values rather than client-controlled ones, and propagate the right application failure?
It does not ask whether PostgreSQL really rolled back, whether the SQL locked the right row, whether the provider request contained the right JSON, whether Spring Security rejected the request or whether JPA mapped the entity correctly; those belong to the other layers of 10.01's strategy. So most use case tests need no @SpringBootTest, no PostgreSQL and no HTTP server. Our use cases receive their dependencies through the constructor, so a test simply calls new CreateOrderUseCase(productRepository, inventoryRepository, orderRepository, clock): production passes PostgreSQL-backed repositories, a test passes doubles, and the constructor is the testing seam. The domain objects stay real: a CancelOrderUseCase test uses a real Order, because cancel() throwing OrderNotCancellable is part of the workflow (10.11 explains why mocking it fails).
Stubs, Fakes and Mocks
We use three kinds of test double:
- A stub gives controlled answers:
ProductRepository.findById(...)returns a Product withpriceCents = 8990, orconsumeIfAvailable(...)returnsfalse. - A fake is a lightweight working implementation. It helps when we want to inspect resulting state ("save the Product, then read it again").
- A mock records interactions.
verify(paymentService, never()).pay(any())matters when aCANCELLEDOrder must not reach the payment provider: the interaction itself is the guarantee.
Here are two fakes. InMemoryProductRepository is the map from 7.02, now that ProductRepository also has the browse query from 7.04; SuccessfulPaymentService approves every payment:
package com.tarkev.ordermanagement.product.application;
import com.tarkev.ordermanagement.product.domain.Product;
import com.tarkev.ordermanagement.product.domain.ProductId;
import com.tarkev.ordermanagement.shared.application.PageQuery;
import com.tarkev.ordermanagement.shared.application.PageResult;
import java.util.*;
class InMemoryProductRepository implements ProductRepository {
private final Map<ProductId, Product> products = new HashMap<>();
@Override
public Optional<Product> findById(ProductId productId) {
return Optional.ofNullable(products.get(productId));
}
@Override
public void save(Product product) {
products.put(product.id(), product);
}
@Override
public PageResult<ProductSummary> findOrderable(PageQuery pageQuery) {
throw new UnsupportedOperationException("Tested against PostgreSQL");
}
}
package com.tarkev.ordermanagement.order.application;
import com.tarkev.ordermanagement.payment.application.*;
class SuccessfulPaymentService implements PaymentService {
@Override
public PaymentResult pay(PaymentRequest request) {
return PaymentResult.succeeded("pay_" + request.orderId().value());
}
}
The fake repository refuses findOrderable(...) on purpose: browsing is SQL joining, filtering and paging, and imitating it in a map would only test the fake. Whether a helper is technically a fake, stub, spy or mock matters less than knowing which behaviour it controls and which guarantee the test proves. Choose the simplest double that makes the test clear; with Mockito one object is often stub and mock at once (we stub findOwnedForUpdate(...) and verify save(...) on the same OrderRepository).
Product Workflows
CreateProductUseCase already has its test from 7.02: with the in-memory fake, it checks that command values become domain values, the Product is active and reached the persistence boundary. It does not prove that the UUID column, the BIGINT mapping or the Flyway migration work; those are repository integration concerns for the next lesson.
UpdateProductUseCase covers name only, price only, both, an empty update and a missing Product. The domain already rejects a blank name or a negative price, so the test looks at orchestration. With a mocked ProductRepository whose findById(...) returns Optional.empty(), execute(...) must throw ProductNotFound and verify(repository, never()).save(any()) must pass. The empty update new UpdateProductCommand(product.id(), null, null) is a no-op, not VALIDATION_ERROR, and gets the same never() check. Do not invent a failure just because an empty mutation feels unusual.
Testing CreateOrderUseCase
CreateOrderUseCase (7.06) rejects duplicate lines, loads every Product, rejects missing and inactive ones, captures the server-side price, consumes Inventory in ProductId order, builds the UNPAID Order for the command's CustomerId and saves it. The scenarios worth proving:
| Scenario | Expected |
|---|---|
| Valid items | UNPAID Order for the command's CustomerId, createdAt from the Clock, current Product priceCents in each OrderItem, Order saved |
| Duplicate Product line | DuplicateProductLine before any repository side effect |
| Missing or inactive Product | ProductNotFound or ProductNotOrderable, no Inventory consumption, no save |
| Insufficient Inventory | InsufficientInventory, Order not saved |
Order creation reads Instant.now(clock), so the test passes Clock.fixed(...) and asserts the exact createdAt: no sleeps, no time ranges, no dependence on the machine clock. The repositories are mocks:
package com.tarkev.ordermanagement.order.application;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.tarkev.ordermanagement.inventory.application.InventoryRepository;
import com.tarkev.ordermanagement.order.domain.*;
import com.tarkev.ordermanagement.product.application.ProductRepository;
import com.tarkev.ordermanagement.product.domain.Product;
import com.tarkev.ordermanagement.product.domain.ProductId;
import java.time.*;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
class CreateOrderUseCaseTest {
private static final Instant NOW = Instant.parse("2026-10-04T12:00:00Z");
private static final CustomerId CUSTOMER = new CustomerId("customer-123");
private final ProductRepository productRepository =
mock(ProductRepository.class);
private final InventoryRepository inventoryRepository =
mock(InventoryRepository.class);
private final OrderRepository orderRepository = mock(OrderRepository.class);
private final CreateOrderUseCase useCase = new CreateOrderUseCase(
productRepository, inventoryRepository, orderRepository,
Clock.fixed(NOW, ZoneOffset.UTC));
private final Product keyboard =
Product.create(ProductId.newId(), "Mechanical Keyboard", 8990);
@Test
void createsUnpaidOrderWithServerSidePrice() {
when(productRepository.findById(keyboard.id()))
.thenReturn(Optional.of(keyboard));
when(inventoryRepository.consumeIfAvailable(keyboard.id(), 2))
.thenReturn(true);
Order order = useCase.execute(new CreateOrderCommand(CUSTOMER,
List.of(new CreateOrderCommand.Item(keyboard.id(), 2))));
assertThat(order.customerId()).isEqualTo(CUSTOMER);
assertThat(order.status()).isEqualTo(OrderStatus.UNPAID);
assertThat(order.createdAt()).isEqualTo(NOW);
assertThat(order.items()).containsExactly(
new OrderItem(keyboard.id(), 2, 8990));
verify(orderRepository).save(order);
}
@Test
void duplicateProductLineIsRejectedBeforeAnySideEffect() {
var line = new CreateOrderCommand.Item(keyboard.id(), 2);
assertThatThrownBy(() -> useCase.execute(
new CreateOrderCommand(CUSTOMER, List.of(line, line))))
.isInstanceOf(DuplicateProductLine.class);
verifyNoInteractions(inventoryRepository, orderRepository);
}
@Test
void inactiveProductStopsTheOrderBeforeInventoryIsTouched() {
Product mouse = Product.create(ProductId.newId(), "Mouse", 2990);
mouse.deactivate();
when(productRepository.findById(keyboard.id()))
.thenReturn(Optional.of(keyboard));
when(productRepository.findById(mouse.id()))
.thenReturn(Optional.of(mouse));
when(inventoryRepository.consumeIfAvailable(any(), anyInt()))
.thenReturn(true);
assertThatThrownBy(() -> useCase.execute(new CreateOrderCommand(
CUSTOMER, List.of(new CreateOrderCommand.Item(keyboard.id(), 1),
new CreateOrderCommand.Item(mouse.id(), 1)))))
.isInstanceOf(ProductNotOrderable.class);
verifyNoInteractions(inventoryRepository, orderRepository);
}
}
A fourth test stubs consumeIfAvailable(keyboard.id(), 5) to return false and expects InsufficientInventory plus verify(orderRepository, never()).save(any()). The assertions are AssertJ's, which spring-boot-starter-test brings along with JUnit and Mockito.
What the Happy Path Proves
The first test protects our authority boundary: CreateOrderCommand has no unitPriceCents or totalCents for a client to fill, so when the stubbed Product costs 8990, the OrderItem must say 8990. It also pins the CustomerId, which the handler takes from currentUser.customerId(); no CustomerRepository, JWT or security context is needed, and we do not add a boolean isAdmin to commands so that unit tests can simulate roles. The Order starts UNPAID without any PaymentService. If constructing CreateOrderUseCase ever needed mock(PaymentService.class), test setup would be revealing architectural drift.
Validation Ordering and Side Effects
The other tests are about what must not happen. A duplicate line is rejected before any Inventory consumption or persistence, so verifyNoInteractions(inventoryRepository, orderRepository) is part of the requirement. Our agreed strategy also validates all Product-level state before Inventory mutation: with an active keyboard and an inactive mouse, the result is PRODUCT_NOT_ORDERABLE and consumption must not even begin. A missing Product behaves the same way; the use case never manufactures a fake Product.
Is that ordering worth a test? Suppose a refactoring consumes each Product's stock as soon as it is loaded, inside toOrderItem(...). Every happy-path test stays green. This one does not:
CreateOrderUseCaseTest > inactiveProductStopsTheOrderBeforeInventoryIsTouched() FAILED
org.mockito.exceptions.verification.NoInteractionsWanted:
No interactions wanted here:
-> at com.tarkev.ordermanagement.order.application.CreateOrderUseCaseTest.inactiveProductStopsTheOrderBeforeInventoryIsTouched(CreateOrderUseCaseTest.java:76)
But found these interactions on mock 'inventoryRepository':
-> at com.tarkev.ordermanagement.order.application.CreateOrderUseCase.toOrderItem(CreateOrderUseCase.java:57)
Actually, above is the only interaction with this mock.
The test stubs consumeIfAvailable(...) to answer true on purpose: stock is available, so only the inactive Product can stop the Order. Without the stub, Mockito's default false would have turned the bug into an InsufficientInventory. Error precedence can be tested the same way (duplicate IDs are rejected before a missing Product is noticed), but only where the ordering was deliberately designed.
What Mocks Cannot Prove About Transactions
With two Products, the mock can answer true for A and false for B. The test proves that the use case throws InsufficientInventory and does not save the Order. It cannot prove that the real PostgreSQL UPDATE for A was rolled back, and a fake can even mislead: an in-memory Inventory that goes from 10 to 8 for A stays at 8 when B fails. A unit test proves application intent; 10.06 proves the actual transaction behaviour.
A Small Fake for Payment Attempts
Cancellation and payment depend on PaymentAttemptRepository (9.08). A fake is clearer than a mock here, because the tests inspect the resulting attempts and PayOrderUseCase reads back the attempt it saved:
package com.tarkev.ordermanagement.order.application;
import com.tarkev.ordermanagement.order.domain.OrderId;
import com.tarkev.ordermanagement.payment.application.PaymentAttemptRepository;
import com.tarkev.ordermanagement.payment.domain.PaymentAttempt;
import com.tarkev.ordermanagement.payment.domain.PaymentAttemptId;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
class InMemoryPaymentAttemptRepository implements PaymentAttemptRepository {
private final List<PaymentAttempt> attempts = new ArrayList<>();
@Override
public void save(PaymentAttempt attempt) {
if (!attempts.contains(attempt)) {
attempts.add(attempt);
}
}
@Override
public Optional<PaymentAttempt> findById(PaymentAttemptId attemptId) {
return attempts.stream()
.filter(attempt -> attempt.id().equals(attemptId)).findFirst();
}
@Override
public Optional<PaymentAttempt> findUnresolvedByOrderId(OrderId orderId) {
return attempts.stream()
.filter(attempt -> attempt.orderId().equals(orderId))
.filter(PaymentAttempt::isUnresolved).findFirst();
}
List<PaymentAttempt> all() {
return List.copyOf(attempts);
}
}
It does not pretend to be PostgreSQL: no partial unique index, no lock, no transaction. It holds the same PaymentAttempt objects the use case mutates, which is enough for orchestration.
Testing CancelOrderUseCase
Cancellation (7.12, with the attempt check from 9.09) coordinates the owned Order, its lifecycle, Inventory restoration and persistence. CancelOrderUseCaseTest builds the use case from a mocked OrderRepository and InventoryRepository plus the fake, and a real Order with a keyboard × 2 and a mouse × 3; ownedOrderIs(order) stubs findOwnedForUpdate(order.id(), CUSTOMER):
@Test
void cancelsOwnedUnpaidOrderAndRestoresExactQuantities() {
ownedOrderIs(order);
Order cancelled = useCase.execute(cancelCommand());
assertThat(cancelled.status()).isEqualTo(OrderStatus.CANCELLED);
verify(inventoryRepository).restore(keyboard, 2);
verify(inventoryRepository).restore(mouse, 3);
verify(orderRepository).save(order);
}
@Test
void paidOrderDoesNotRestoreInventory() {
order.markPaid();
ownedOrderIs(order);
assertThatThrownBy(() -> useCase.execute(cancelCommand()))
.isInstanceOf(OrderNotCancellable.class);
verifyNoInteractions(inventoryRepository);
verify(orderRepository, never()).save(any());
}
Two more tests complete the class. findOwnedForUpdate(...) returns Optional.empty() both for a missing Order and for another customer's Order, so one empty stub covers both: OrderNotFound, no restoration. And with a PENDING attempt saved in the fake (PaymentAttempt.start(...)), cancellation ends in PaymentInProgress and the Order stays UNPAID. An already CANCELLED Order fails like the PAID one: no second restoration. Restoration runs in ProductId order to reduce deadlock risk (7.12); because that ordering is part of the correctness strategy, a focused test may check it with Mockito's inOrder(...), but avoid asserting incidental iteration order anywhere else.
Testing PayOrderUseCase
Payment is the richest workflow: ownership, payability, a durable attempt, the PaymentService call and the local application of success, decline or uncertainty (9.09). We control PaymentService without contacting a provider: SuccessfulPaymentService above, or a mock that returns PaymentResult.succeeded(...) or PaymentResult.declined(...), or throws PaymentOutcomeUnknown. The use case also needs a TransactionTemplate; a real one around a mocked PlatformTransactionManager simply runs each callback, since the mock hands out no transaction and its commit(...) does nothing. The test stays plain Java and proves orchestration, not transaction boundaries.
package com.tarkev.ordermanagement.order.application;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.tarkev.ordermanagement.order.domain.*;
import com.tarkev.ordermanagement.payment.application.*;
import com.tarkev.ordermanagement.payment.domain.PaymentAttemptStatus;
import com.tarkev.ordermanagement.product.domain.ProductId;
import java.time.*;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
class PayOrderUseCaseTest {
private static final Instant NOW = Instant.parse("2026-10-04T12:00:00Z");
private static final CustomerId CUSTOMER = new CustomerId("customer-123");
private final OrderRepository orderRepository = mock(OrderRepository.class);
private final InMemoryPaymentAttemptRepository attempts =
new InMemoryPaymentAttemptRepository();
private final PaymentService paymentService = mock(PaymentService.class);
private final PayOrderUseCase useCase = new PayOrderUseCase(
orderRepository, attempts, paymentService,
new TransactionTemplate(mock(PlatformTransactionManager.class)),
Clock.fixed(NOW, ZoneOffset.UTC));
private final Order order = Order.create(OrderId.newId(), CUSTOMER,
List.of(new OrderItem(ProductId.newId(), 2, 8990),
new OrderItem(ProductId.newId(), 1, 6950)),
NOW);
@Test
void chargesTheOrderTotalAndMarksTheOrderPaid() {
ownedOrderIs(order);
when(paymentService.pay(any()))
.thenReturn(PaymentResult.succeeded("pay_123"));
Order paid = useCase.execute(payCommand());
verify(paymentService).pay(new PaymentRequest(order.id(), 24930));
assertThat(paid.status()).isEqualTo(OrderStatus.PAID);
assertThat(attempts.all()).singleElement().satisfies(attempt -> {
assertThat(attempt.status()).isEqualTo(PaymentAttemptStatus.SUCCEEDED);
assertThat(attempt.providerReference()).isEqualTo("pay_123");
});
}
@Test
void unknownOutcomeIsNotADeclineAndBlocksASecondCharge() {
ownedOrderIs(order);
when(paymentService.pay(any())).thenThrow(
new PaymentOutcomeUnknown("read timeout", null));
assertThatThrownBy(() -> useCase.execute(payCommand()))
.isInstanceOf(PaymentOutcomeUnknown.class);
assertThat(order.status()).isEqualTo(OrderStatus.UNPAID);
assertThat(attempts.all()).singleElement().extracting("status")
.isEqualTo(PaymentAttemptStatus.UNKNOWN);
assertThatThrownBy(() -> useCase.execute(payCommand()))
.isInstanceOf(PaymentInProgress.class);
verify(paymentService).pay(any());
}
private void ownedOrderIs(Order owned) {
when(orderRepository.findOwnedForUpdate(owned.id(), CUSTOMER))
.thenReturn(Optional.of(owned));
}
private PayOrderCommand payCommand() {
return new PayOrderCommand(CUSTOMER, order.id());
}
}
What the Payment Tests Prove
The amount comes from the Order: 2 × 8990 + 6950 = 24930 cents. PaymentRequest is a record, so verify(paymentService).pay(new PaymentRequest(order.id(), 24930)) checks the exact request (an ArgumentCaptor works too for field-by-field assertions). No client-controlled amount reaches the provider.
The full class has three more tests. A CANCELLED Order (after order.cancel()) must end in OrderNotPayable with verifyNoInteractions(paymentService) and no attempt, and a non-owned Order (an empty lookup) in OrderNotFound the same way: "no external charge" is a financial guarantee. A decline (PaymentResult.declined("insufficient_funds")) ends in PaymentDeclined, an UNPAID Order and a DECLINED attempt, with no cancellation and no Inventory change. An unknown outcome is not a decline: the attempt is UNKNOWN, the Order stays UNPAID, and a second pay request ends in PaymentInProgress while verify(paymentService).pay(any()) confirms exactly one provider call. Once a recovery path exists, the next test is that a known success is applied locally without calling PaymentService again.
The assertions use the real PaymentAttemptStatus values from 9.08. Inventing TIMEOUT_RETRY_PENDING only because a test needs something to assert would be backwards: tests follow production semantics. The three order test classes need well under a second of test time (Gradle's test report); JVM and Gradle startup cost more than the tests:
Class Tests Duration
CreateOrderUseCaseTest 4 0.516s
CancelOrderUseCaseTest 4 0.007s
PayOrderUseCaseTest 5 0.065s
13 tests, 0 failures, 0 skipped
Interaction Assertions
The verify(...) calls above all guard a side effect where a call means money or stock: no provider call for an ineligible Order, no consumption after failed Product validation, no save after insufficient Inventory, no restoration for a disallowed cancellation, exact quantities for a valid one. "findById called exactly once" or verifyNoMoreInteractions(...) merely because that is how the current code looks would only break harmless refactorings; 10.11 draws that line in detail.
Failures and Exceptions
Suppose productRepository.save(any()) throws new RuntimeException("database unavailable"). The use case must not catch Exception and turn it into ProductNotFound or InsufficientInventory; a use case test can verify that the unexpected failure propagates, and its conversion to 500 INTERNAL_ERROR belongs to API testing. In the other direction, use case tests assert application exceptions such as OrderNotPayable, never HTTP statuses: the API test asserts 409 ORDER_NOT_PAYABLE.
Double the Contracts the Use Case Owns
Prefer doubling OrderRepository over EntityManager, and PaymentService over RestClient: those are the dependencies the use case owns. If constructing CreateOrderUseCase required mocks for EntityManager, HttpServletRequest or SecurityContextHolder, that would signal boundary leakage. Keep fakes small, too: if a test only needs findOwnedForUpdate(...) to return empty, a stub beats a complete in-memory OrderRepository.
@Mock fields with @ExtendWith(MockitoExtension.class) work as well as mock(...); the extension's strict stubs fail a test whose stubbing is never used, so a deliberate "stock is available" stub like ours would need lenient().
Practice Exercise 1: Complete the Suites
Solutions: lesson 10.14.
Write the missing cases in two new plain JUnit classes, so the lesson's classes stay as they are. M10OrderUseCaseGapsTest in order.application uses a real Order of Standing Desk × 1 at 24900 cents and Desk Lamp × 3 at 3490 cents:
- Create Order: a missing Product ends in
ProductNotFoundwith no consumption and no save; two valid lines each carry their own Product's price, and each line's stock is consumed with its own quantity. - Cancel Order: an already
CANCELLEDOrder restores nothing and is not saved; restoration followsProductIdorder, checked withinOrder(...). - Pay Order: with
SuccessfulPaymentServicethe Order endsPAIDwith oneSUCCEEDEDattempt for 35370 cents; aPAIDOrder never reaches the provider; aPaymentProviderUnavailablepropagates and leaves the attemptUNKNOWN(9.09).
M10CatalogUseCaseTest in inventory.application covers DeactivateProductUseCase (a missing Product throws ProductNotFound and nothing is saved) and SetInventoryUseCase (a missing Product is rejected before Inventory is touched; the first quantity for a Product without Inventory creates it). For each test, decide what is stubbed, what is real and which interaction is verified.
Practice Exercise 2: Classify the Test
Which layer proves each guarantee: PaymentService is not called for a CANCELLED Order; a provider timeout is detected after the configured duration; the PAID status and the SUCCEEDED attempt commit together; POST /api/v1/orders/{id}/pay returns 409 ORDER_NOT_PAYABLE? And does a cancellation test whose mocked restore(B, ...) throws prove that PostgreSQL rolls back restoration A?
Knowledge Check
Question 1
Why does inactiveProductStopsTheOrderBeforeInventoryIsTouched stub consumeIfAvailable(...) to return true?
Question 2
Why does PayOrderUseCaseTest fake the payment attempts but mock PaymentService?
Question 3
Why would verifyNoMoreInteractions(...) on every mock make the suite worse?
Knowledge Check Answers
Answer 1
Mockito's default false would turn a consumption bug into InsufficientInventory. With stock available, only the inactive Product can stop the Order, so the test fails for the right reason.
Answer 2
The test reads back the resulting attempts, and the use case reads back the attempt it saved, which a small working fake supports. For PaymentService, the guarantee is the interaction itself: called once with the right amount, or never.
Answer 3
It pins down every incidental call, so harmless refactorings break tests. Verification belongs on calls that move money or stock.
Engineering Principle
Use case tests verify orchestration against application boundaries. Replace infrastructure with controlled doubles, but keep real domain objects so business behavior is still exercised.
A mock-based test can prove that the application asked a repository to do something; it cannot prove that the database did it atomically or correctly.
Summary
- Use case tests check application workflow in plain Java: constructor injection lets us pass doubles, with no Spring context, PostgreSQL or HTTP server.
- Domain objects stay real; stubs, fakes and mocks sit at the boundaries, and the simplest clear double wins.
CreateOrderUseCasetests prove server-side pricing, the command'sCustomerId,UNPAIDcreation with a fixedClock, and that invalid requests stop before later side effects.CancelOrderUseCasetests prove ownership, lifecycle and unresolved-payment checks before restoration, and exact restored quantities.PayOrderUseCasetests controlPaymentServiceand fake the attempts: ineligible Orders never reach the provider, the amount isorder.totalCents(), and an unknown outcome blocks a second charge.- Interaction assertions protect correctness-critical effects, especially negative ones; over-verification breaks harmless refactoring.
- Use case tests assert application exceptions, not HTTP statuses, and let technical failures propagate.
- Unit tests prove orchestration intent; rollback, locks, constraints, mappings and transaction boundaries need infrastructure tests.
In the next lesson, repository Integration Testing with PostgreSQL, we move below the application repository interfaces and test the real persistence adapters against PostgreSQL: Flyway migrations, JPA mappings, column types, constraints, Order and OrderItem persistence, customer-scoped queries, deterministic pagination and the atomic Inventory SQL.
Think first
Coming soonCheckpoint questions that ask you to commit to an answer before the lesson shows it.