Reading.CleanArchitecture.TestBoundary

1. The Importance of Boundaries in Testing

  • Boundaries separate different components or layers of a system, such as the business logic, UI, database, and external APIs.
  • Well-defined boundaries make it possible to:
    • Test components in isolation.
    • Mock or stub external systems and dependencies.
    • Write more focused and reliable tests.

2. Types of Tests and Boundaries

a. Unit Tests
  • Unit tests focus on individual components (e.g., functions, classes) and ensure their correctness in isolation.
  • Boundaries allow you to mock or replace external dependencies (e.g., database, API) in unit tests.

Example: Testing a Use Case

@Test
public void testCalculateTotalPrice() {
    Order order = new Order();
    order.addProduct(new Product("Item1", 10.0));
    order.addProduct(new Product("Item2", 15.0));

    double total = order.calculateTotal();

    assertEquals(25.0, total, 0.01);
}
b. Integration Tests
  • Test interactions between components across boundaries, such as:
    • Use cases interacting with repositories.
    • Controllers calling use cases.
  • These tests ensure that different parts of the system work together as expected.

Example: Testing a Repository

@Test
public void testSaveOrder() {
    OrderRepository repository = new SqlOrderRepository();
    Order order = new Order();
    repository.save(order);

    Order retrieved = repository.findById(order.getId());
    assertNotNull(retrieved);
}
c. Acceptance Tests
  • End-to-end tests that validate the entire system from the user’s perspective.
  • These tests cross all boundaries, including the UI, use cases, and database.

Example: Testing an API Endpoint

@Test
public void testPlaceOrderEndpoint() {
    Response response = httpClient.post("/orders", orderJson);
    assertEquals(200, response.getStatusCode());
    assertTrue(response.getBody().contains("Order placed successfully"));
}

3. Mocking and Stubbing Across Boundaries

Mocking External Systems:
  • Use mocks to simulate the behavior of external systems like APIs, databases, or third-party services.
  • This ensures that tests remain fast and reliable without depending on external factors.

Example: Mocking a Payment Gateway

PaymentGateway mockGateway = Mockito.mock(PaymentGateway.class);
Mockito.when(mockGateway.processPayment(order)).thenReturn(true);
Stubbing Repositories:
  • Replace actual database calls with in-memory implementations for testing business logic.
  • Example: Use an in-memory repository for testing a use case.
public class InMemoryOrderRepository implements OrderRepository {
    private Map<String, Order> orders = new HashMap<>();

    public void save(Order order) {
        orders.put(order.getId(), order);
    }

    public Order findById(String id) {
        return orders.get(id);
    }
}

4. Benefits of Testing at Boundaries

  • Isolation: Boundaries isolate the system into testable units, enabling focused and reliable tests.
  • Speed: By mocking or stubbing dependencies, tests run faster.
  • Reliability: Reduces flakiness caused by external systems like APIs or databases.
  • Clarity: Tests can focus on a single responsibility, improving readability and maintainability.

5. Challenges in Testing Boundaries

Boundary Leaks:
  • Poorly defined boundaries can lead to dependencies between layers, making tests brittle or hard to write.
  • Example: Business logic depending directly on a database schema.
Over-Mocking:
  • Excessive mocking can lead to tests that pass even when the system is broken because they don’t validate real-world scenarios.
  • Balance mocks with integration and acceptance tests.

6. Testing the Layers in Clean Architecture

Entities:
  • Entities contain core business rules and are the easiest to test.
@Test
public void testOrderDiscount() {
    Order order = new Order();
    order.addProduct(new Product("Item1", 100.0));
    order.applyDiscount(10);

    assertEquals(90.0, order.calculateTotal(), 0.01);
}
Use Cases:
  • Use cases involve workflows and coordinate between entities and boundaries.
  • Test them by mocking repositories and other dependencies.

Example:

@Test
public void testPlaceOrderUseCase() {
    OrderRepository mockRepository = Mockito.mock(OrderRepository.class);
    PlaceOrderUseCase useCase = new PlaceOrderUseCase(mockRepository);

    Order order = new Order();
    useCase.execute(order);

    Mockito.verify(mockRepository).save(order);
}
Interface Adapters:
  • Controllers, presenters, and gateways are tested by verifying their interactions with the use case layer.

Example:

@Test
public void testOrderController() {
    PlaceOrderUseCase mockUseCase = Mockito.mock(PlaceOrderUseCase.class);
    OrderController controller = new OrderController(mockUseCase);

    ResponseEntity<String> response = controller.placeOrder(order);

    assertEquals(HttpStatus.OK, response.getStatusCode());
    Mockito.verify(mockUseCase).execute(order);
}
Frameworks and Drivers:
  • Framework-specific code (e.g., database drivers, web frameworks) is tested with integration or acceptance tests.

Key Takeaways

  1. Boundaries Simplify Testing:
    • Clear boundaries allow you to mock, stub, or isolate dependencies for focused tests.
  2. Layer-Specific Testing:
    • Test each layer (entities, use cases, interface adapters) with appropriate strategies (unit, integration, acceptance).
  3. Use Mocks and Stubs Judiciously:
    • Avoid over-mocking and balance with real-world integration tests.
  4. Focus on Core Business Logic:
    • Ensure business rules are isolated and tested independently of external systems.
This entry was posted in Без рубрики. Bookmark the permalink.