Skip to content

Rayllienstery/LibraryBooks

Repository files navigation

[Medium] iOS&macOS CleanArchitecture.Part-3 UseCase and Repository testing

LibraryBooks – Clean Architecture Example Project

A minimal, production‑grade example demonstrating how UseCase, Repository, and ViewModel interact inside a Clean Architecture iOS/macOS project.

This project exists for one purpose:
to show how a cleanly isolated domain can be tested without touching UI, storage, or infrastructure.

The domain is intentionally simple — a small library system.
It is “grown‑up” enough to represent real business logic, but minimal enough that nothing distracts from the architecture.


Purpose of the Project

Modern iOS apps rely on business logic that must stay independent from the interface, storage engine, or networking layer.
This project demonstrates how to structure such logic using:

  • Domain UseCases — the autonomous core of the application
  • Repositories — abstract data access boundaries
  • ViewModels — thin mediators used by SwiftUI
  • Mocks — isolated test doubles that allow deterministic testing

The idea is straightforward:

If your UseCase works correctly with a mock repository,
it will work correctly with a real one — and without fragile, UI‑driven tests.

This repository is designed to be examined reading top‑down, from the conceptual architecture to the concrete implementation.


Project Structure

LibraryBooks/
│
├─ Presentation/
│   └─ BookList/
│       ├─ BookListView.swift
│       └─ BookListViewModel.swift
│
├─ Domain/
│   ├─ Entities/
│   │   └─ Book.swift
│   ├─ UseCases/
│   │   ├─ BookUseCase.swift
│   │   └─ BookUseCaseImpl.swift
│   └─ Errors/
│       └─ BookError.swift
│
├─ Data/
│   └─ Repositories/
│       ├─ BookRepository.swift
│       └─ BookRepositoryInMemory.swift
│
└─ Tests/
    ├─ Support/
    │   ├─ BookRepositoryMock.swift
    │   ├─ BookUseCaseMock.swift
    │   └─ BookMocks.swift
    ├─ BookRepositoryTests.swift
    ├─ BookUseCaseTests.swift
    └─ BookListViewModelTests.swift

Clean Architecture Layers

Presentation Layer

  • Contains BookListViewModel
  • Fetches available books
  • Handles borrow operations
  • Reacts to UseCase outputs (success or error)
  • Holds no business rules

Domain Layer

  • Contains:
    • The Book entity
    • UseCase protocol (BookUseCase)
    • UseCase implementation (BookUseCaseImpl)
    • Domain errors (BookError)

This layer models what the application does, not how it is persisted or rendered.

Data Layer

  • Contains repository protocol + an in‑memory implementation.
  • Responsible only for reading/updating storage.
  • Storage Type → replaceable at any moment:
    • SwiftData
    • CoreData
    • SQLite
    • JSON
    • Network
    • hybrid or mocked storage

Thanks to the repository boundary, switching the underlying engine does not touch the domain layer.


Domain Model

struct Book {
    let id: UUID
    let title: String
    var isBorrowed: Bool
}

A minimal, strictly defined entity without behavior.
It contains no validation, no methods, and no implicit logic.
All decisions stay inside the UseCase.


UseCase

BookUseCase describes the business rules:

  • getAvailableBooks()
    Returns only books not currently borrowed.

  • borrowBook(id:)
    Marks a book as borrowed.

UseCase is intentionally synchronous to keep tests simple and focused on business logic, not concurrency.

Key property:

UseCase does not know where books come from or how they are saved.

This is the cornerstone of its testability.


Repository

BookRepository exposes the minimal required API:

  • fetchAll() — returns all books
  • save(_:) — saves or updates a book

This boundary forces the Data layer to stay simple and predictable.

The default implementation is an in‑memory store.
It is intentionally small — the goal is clarity, not infrastructure.
Methods are synchronous to keep the focus on business logic testing, not async complexity.


ViewModel

BookListViewModel is a UI‑agnostic reactive wrapper around the UseCase.

Responsibilities:

  • load available books
  • borrow a book
  • expose state (books, error)
  • hold no side effects other than communicating with the UseCase

Because it contains no domain rules, it becomes trivial to test.

Note: For the purposes of this article, a separate "Borrowed Books" list is intentionally omitted. The focus is on demonstrating how to test business logic (filtering available books) rather than building a complete UI. Adding a borrowed books list would add complexity without contributing to the core testing concepts.


Testing Strategy

The project demonstrates a deterministic testing approach:

1. UseCase Tests

Test the business logic in isolation:

  • availability filtering
  • borrow operations
  • correct error propagation

2. Repository Tests

Test the data layer boundary:

  • fetchAll returns all books
  • save adds new books
  • save updates existing books

3. ViewModel Tests

Ensure correct presentation behavior:

  • loads available books from use case
  • handles errors gracefully
  • updates state after borrow operations

4. No UI Tests

Because business logic is isolated, 90% of correctness is proven via unit tests.


Why This Testing Works

The testing approach in this project demonstrates a fundamental principle of Clean Architecture:

If your UseCase works correctly with a mock repository, it will work correctly with any real data source.

Isolation Removes Noise

By testing business logic in isolation:

  • No UI dependencies: Tests don't need SwiftUI views or user interactions
  • No infrastructure dependencies: Tests don't need databases, networks, or file systems
  • Fast execution: Tests run in milliseconds, not seconds
  • Deterministic results: Same inputs always produce same outputs

The Mock Contract

The BookRepositoryMock implements the same protocol as BookRepositoryInMemory. This means:

  • UseCase doesn't know it's talking to a mock
  • Business logic is tested independently of storage implementation
  • You can swap implementations without changing tests

What This Proves

When BookUseCaseTests pass, they prove:

  1. Business rules are correct (filtering, validation)
  2. Error handling works (bookNotFound, bookAlreadyBorrowed)
  3. State mutations are correct (isBorrowed flag updates)

These tests give confidence that the business logic will work correctly whether the repository uses:

  • In-memory storage (current implementation)
  • SwiftData (future implementation)
  • Core Data (future implementation)
  • Network API (future implementation)

This is the power of Clean Architecture: test once, deploy anywhere.


Key Takeaways

  • Clean Architecture allows business logic to evolve without rewriting UI or persistence.
  • UseCase sits at the center — it is the single source of truth for decisions.
  • Repository abstraction keeps the domain independent from storage details.
  • ViewModel is intentionally thin — a reactive bridge, not a logic container.
  • Tests become small, meaningful, and deterministic.

This project is intentionally minimal.
It serves as a reference for anyone who wants to build scalable, testable Swift applications using a layered architecture.


Extending the Project

You can easily modify or expand it:

  • Swap the repository with a real database
  • Add networking
  • Add a “returnBook” feature
  • Add reservations or due dates
  • Add caching or offline mode
  • Integrate into a larger app

The architecture remains stable, and each new feature grows naturally inside the existing boundaries.


License

Apache 2.0 License.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages