A minimal, production-grade SwiftUI demo that showcases how to architect for testability in Swift using the new Swift Testing Framework, @Mockable, and @Spyable. Built under the principles of Clean Architecture.
Published by Code with Shabib — codewithshabib.com
This project exists to:
- Demonstrate testable Clean Architecture in a real SwiftUI app
- Showcase how to use Mockable and Spyable for Swift-native test double generation
- Cover the entire spectrum of test doubles: mock, stub, spy, fake, and dummy
- Provide a reusable base for unit testing, SwiftUI previews, and architectural validation
This demo project is designed to complement the comprehensive blog post "Architecting for Testability in Swift: A Deep Dive into Mockable, Spyable, and the New Swift Testing Framework" - Read the full article.
The blog post provides:
- Detailed explanations of testing principles and strategies
- Cross-platform testing ecosystem analysis
- Organizational testing strategy guidance
- Future evolution of Swift testing
- Real-world examples and best practices
While this project serves as a practical implementation of those concepts, demonstrating:
- Clean Architecture in action
- Test double spectrum implementation
- SwiftUI preview strategies
- Unit testing patterns
- Integration testing approaches
Together, they provide a complete learning resource for modern Swift testing.
This app uses strict layering:
SwiftUI View
↓
ViewModel (presentation logic)
↓
UseCase (domain logic)
↓
Repository (protocol)
↓
API Service (data layer)
Each layer depends only inward, aligned with Clean Architecture.
- DTOs: API-specific data structures
- Entities: Pure domain models
- ViewData: Presentation-layer structs mapped from domain models
- Mappers: Pure functions between layers (no mocking needed)
SimpleWeather/
├── WeatherView/
│ ├── Presentation/ (SwiftUI View, ViewModel, ViewData)
│ ├── Domain/ (Entities, Use Cases, Repository interfaces)
│ ├── Data/ (Repository impls, API services, DTOs)
│ └── Shared/ (Utilities, test doubles, etc.)
└── SimpleWeatherTests/
└── Unit tests for all layers
This project explicitly demonstrates all 5 types of test doubles:
Type | Used In | Example File(s) / Usage |
---|---|---|
✅ Dummy | DummyLogger , DummyAnalyticsTracker |
WeatherView/Shared/Utilities/Dummies/ Used in previews and as default dependencies |
✅ Stub | WeatherAPIServiceStub (configurable return values) |
WeatherView/Data/Stubs/WeatherAPIServiceStub.swift Used in previews and tests |
✅ Mock | MockWeatherRepositoryProtocol , MockFetchWeatherUseCaseProtocol (via Mockable) |
Used in unit tests: SimpleWeatherTests/FetchWeatherUseCase.swift etc. |
✅ Spy | AnalyticsTrackerSpy , LoggerSpy (via Spyable) |
Used in unit tests: SimpleWeatherTests/WeatherViewModelTests.swift |
✅ Fake | FakeWeatherAPIService (returns hardcoded but plausible data) |
WeatherView/Data/Fakes/FakeWeatherAPIService.swift Used in previews and tests |
Mocks and spies are generated using the Mockable and Spyable frameworks:
@mockable protocol FetchWeatherUseCaseProtocol { ... }
@spyable protocol AnalyticsTracker { ... }
Generated mocks/spies are shared between:
- ✅ Unit tests
- ✅ SwiftUI previews
- ✅ Integration-style flows
WeatherAPIService
: talks to Open-MeteoWeatherRepository
: translates DTO to domainFetchWeatherUseCase
: business logic layerWeatherViewModel
: testable, observable presentation logicLogger
+AnalyticsTracker
: abstracted observabilityFakeWeatherAPIService
: replaces real API for local testing
Here's how to use stubs in SwiftUI previews:
// Using a stub with configurable return values
let stub = WeatherAPIServiceStub(
weatherToReturn: WeatherResponseDTO(
temperature: 28.0,
condition: "Sunny"
)
)
let repository = WeatherRepository(api: stub)
let useCase = FetchWeatherUseCase(repository: repository)
let viewModel = WeatherViewModel(
fetchWeatherUseCase: useCase,
logger: DummyLogger() // Using dummy for dependencies we don't care about
)
return WeatherView(viewModel: viewModel)
And here's how to use fakes for previews:
// Using a fake with fixed behavior
let fakeAPI = FakeWeatherAPIService()
let repository = WeatherRepository(api: fakeAPI)
let useCase = FetchWeatherUseCase(repository: repository)
let viewModel = WeatherViewModel(fetchWeatherUseCase: useCase)
// Fetch the weather when preview loads
Task {
await viewModel.fetchWeather()
}
return WeatherView(viewModel: viewModel)
These approaches ensure UI previews can demonstrate different states without real networking.
Most Swift projects struggle with testability and architectural boundaries. This project demonstrates principles that you can adapt to your own architectural decisions:
- Apply dependency inversion principles strategically
- Create previewable, testable SwiftUI components
- Leverage code generation to reduce boilerplate
Understanding these patterns will help inform your own architectural decisions and testing strategies.
This project demonstrates several testing patterns:
- Given-When-Then structure in all test cases
- Dependency injection via protocols for testability
- Arrange-Act-Assert pattern in test methods
- Verification of interactions via Mockable's
verify()
API - Capturing arguments and return values for assertions
- Testing async/await code with the Swift Testing framework
- Clone this repository
- Open
SimpleWeather.xcodeproj
in Xcode 16.3 or later - Build and run the project
To run the tests:
# From Xcode: Product > Test or ⌘U
# From command line:
xcodebuild test -scheme SimpleWeather -destination 'platform=iOS Simulator,name=iPhone 16'
See the SimpleWeather app in action:
The demo shows the clean UI and weather fetching functionality in action, highlighting the architecture's simplicity and effectiveness.
This project is licensed under the MIT License - see the LICENSE file for details.