β οΈ Alpha Version: TestDRS is currently in alpha and the API may change. We welcome feedback and contributions!
Test DRS (Dependency Replacement System) is a modern Swift testing library that accelerates your testing velocity with lightning-fast spying, stubbing, and mocking capabilities. Built with Swift macros, TestDRS provides type-safe, compiler-verified test doubles that integrate seamlessly with both XCTest and Swift Testing frameworks.
- Features
- Requirements
- Installation
- Quick start
- Core features
- Testing patterns
- Documentation
- Examples
- Contributing
- License
- β¨ Zero Boilerplate: Generate mocks with simple macro annotations
- π Type-Safe: Compile-time verification of mock implementations
- π Swift Concurrency: First-class support for async/await testing
- π― Flexible: Mock protocols, classes, and structs
- π§ͺ Framework Agnostic: Works with XCTest and Swift Testing
- π Rich Verification: Comprehensive call verification and parameter matching
- β‘ Static Testing: Isolated mocks of static members
- π§ Debugging: Clear error messages and debug descriptions
- Swift: 6.0 or later
- Platforms:
- macOS 13.0+
- iOS 16.0+
- tvOS 13.0+
- watchOS 6.0+
- macCatalyst 13.0+
Add TestDRS to your project by adding the following dependency to your Package.swift
file:
dependencies: [
.package(url: "https://github.com/open-turo/swift-test-drs", from: "X.X.X")
]
Then add TestDRS to your target:
.target(
name: "YourTargetName",
dependencies: ["TestDRS"]
)
- In Xcode, go to File β Add Package Dependencies
- Enter the repository URL:
https://github.com/open-turo/swift-test-drs
- Select your desired version
- Add TestDRS to your test target
Here's a simple example to get you started with TestDRS:
import TestDRS
import XCTest
// 1. Add the @AddMock macro to generate a mock
@AddMock
protocol WeatherService {
func fetchWeather(for city: String) async throws -> Weather
}
// 2. Use the generated mock in your tests
final class WeatherViewModelTests: XCTestCase {
func testFetchWeather() async throws {
// Create mock instance
let mockWeatherService = MockWeatherService()
let viewModel = WeatherViewModel(weatherService: mockWeatherService)
// Stub method behavior
let expectedWeather = Weather(temperature: 72, condition: .sunny)
#stub(mockWeatherService.fetchWeather, returning: expectedWeather)
// Execute code under test
try await viewModel.loadWeather(for: "San Francisco")
// Verify results and interactions
XCTAssertEqual(viewModel.currentWeather, expectedWeather)
#expectWasCalled(mockWeatherService.fetchWeather, with: "San Francisco")
.exactlyOnce()
}
}
TestDRS offers two primary approaches for creating mocks:
Apply @AddMock
to your production types to automatically generate mock implementations:
@AddMock
protocol NetworkService {
var timeout: TimeInterval { get set }
func fetchData(from url: URL) async throws -> Data
}
@AddMock
class DatabaseManager {
func save(_ data: Data) throws
func fetch(id: String) -> Data?
}
Benefits:
- Keeps mocks in sync with production code
- Available in debug builds for SwiftUI previews
- Zero maintenance overhead
Create dedicated mock types in your test target:
@Mock
class MockNetworkService: NetworkService {
var timeout: TimeInterval
func fetchData(from url: URL) async throws -> Data
}
Benefits:
- Complete control over mock location
- Not included in production target
- Explicit mock definitions
Control mock behavior with powerful stubbing capabilities:
#stub(mockService.fetchData, returning: expectedData)
#stub(mockService.timeout, returning: 30.0)
#stub(mockService.fetchData, throwing: NetworkError.connectionFailed)
#stub(mockService.fetchData, using: { url in
if url.host == "api.example.com" {
return mockData
} else {
throw NetworkError.invalidURL
}
})
Comprehensive verification of mock interactions:
#expectWasCalled(mockService.fetchData)
#expectWasNotCalled(mockService.deleteData)
#expectWasCalled(mockService.fetchData, with: expectedURL)
#expectWasCalled(mockService.logEvent, with: "user_login", ["id": "123"], 1)
// Exactly once
#expectWasCalled(mockService.fetchData).exactlyOnce()
// Specific count
#expectWasCalled(mockService.retry).occurring(times: 3)
// Range
#expectWasCalled(mockService.poll).occurringWithin(times: 2...5)
For non-Equatable parameters, access the actual call data:
let result = #expectWasCalled(mockService.processComplexData).exactlyOnce()
let call = try result.getMatchingCall()
let (data, options) = call.input
XCTAssertEqual(data.id, "expected-id")
XCTAssertTrue(options.preserveMetadata)
TestDRS provides first-class support for async/await testing:
func testAsyncOperation() async throws {
let mockService = MockAsyncService()
#stub(mockService.fetchData, returning: expectedData)
systemUnderTest.service = mockService
let result = try await systemUnderTest.performOperation()
#expectWasCalled(mockService.fetchData)
XCTAssertEqual(result, expectedResult)
}
For scenarios where you can't directly await operations:
func testNotificationHandler() async {
let mockService = MockService()
let handler = NotificationHandler(service: mockService)
// Trigger notification
NotificationCenter.default.post(name: .dataUpdated, object: nil)
// Wait for async call triggered by notification
await #confirmationOfCall(to: mockService.processUpdate)
.exactlyOnce()
}
Test code that uses static methods and properties with proper isolation:
@AddMock
class Logger {
static func log(_ message: String, level: LogLevel = .info)
static var isEnabled: Bool
}
func testLogging() {
withStaticTestingContext {
// Configure mock behavior
#stub(MockLogger.isEnabled, returning: true)
systemUnderTest.logger = MockLogger.self
// Test code that uses Logger.log(...)
systemUnderTest.performAction()
// Verify static method calls
#expectWasCa
8000
lled(MockLogger.log, with: "Action completed", .info)
}
}
Test callbacks and completion handlers:
@Mock
struct CallbackHandler {
func onComplete(result: Result<Data, Error>)
}
func testCallback() {
let mockHandler = CallbackHandler()
dataLoader.loadData(completion: mockHandler.onComplete)
#expectWasCalled(mockHandler.onComplete)
.exactlyOnce()
}
Works seamlessly with both testing frameworks:
import XCTest
import TestDRS
final class MyTests: XCTestCase {
func testExample() throws {
// TestDRS code here
}
}
import Testing
import TestDRS
struct MyTests {
@Test func example() throws {
// TestDRS code here
}
}
The repository includes a small example project demonstrating TestDRS usage:
We welcome contributions to TestDRS! Whether you're fixing bugs, adding features, improving documentation, or helping with issues, your contributions are valued.
- Report bugs by opening GitHub issues
- Request features or suggest improvements
- Submit code via pull requests
- Improve documentation and examples
- Help others by answering questions in issues
For larger changes, please open an issue first to discuss your approach. This helps ensure your contribution aligns with the project's direction and prevents duplicate work.
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/YOUR_USERNAME/swift-test-drs.git cd swift-test-drs
- Open the package in Xcode:
open Package.swift
- Swift style: Follow standard Swift conventions
- Testing: Add tests for new features and bug fixes
- Documentation: Document public APIs with DocC comments
- Compatibility: Ensure changes work across supported platforms
# Run all tests
swift test
# Run specific test target
swift test --filter TestDRSTests
# Run macro tests
swift test --filter TestDRSMacrosTests
- Create a feature branch (
git checkout -b feature/your-feature
) - Make your changes with tests
- Ensure all tests pass locally
- Commit with clear, descriptive messages
- Push to your fork (
git push origin feature/your-feature
) - Open a pull request with:
- Clear description of changes
- Link to related issues
# Generate DocC documentation
./Scripts/build_docc.sh
TestDRS is available under the MIT license. See the LICENSE file for more info.
Maintained by Turo Open Source
Accelerate your testing velocity with TestDRS π