Section 10/171 menit
10. Testing Strategies dengan Tuist
10. Testing Strategies dengan Tuist
Test Plans per Layer
swift
// Tuist/ProjectDescriptionHelpers/TestPlan+Strategy.swift
import ProjectDescription
public extension TestPlan {
// Fast feedback: hanya unit test tanpa UI test
static var unitTests: Path {
"TestPlans/UnitTests.xctestplan"
}
// Full coverage: semua test termasuk UI test
static var allTests: Path {
"TestPlans/AllTests.xctestplan"
}
// Critical path: test untuk core modules saja
static var coreTests: Path {
"TestPlans/CoreTests.xctestplan"
}
}
swift
// TestPlans/UnitTests.xctestplan
{
"configurations": [
{
"id": "unit-tests",
"name": "Unit Tests",
"options": {
"codeCoverageEnabled": true,
"maximumTestExecutionTimeAllowance": 30,
"testTimeoutsEnabled": true
}
}
],
"testTargets": [
{"target": {"name": "NetworkKitTests"}, "enabled": true},
{"target": {"name": "AuthKitTests"}, "enabled": true},
{"target": {"name": "FeatureHomeTests"}, "enabled": true},
{"target": {"name": "FeatureCheckoutTests"}, "enabled": true},
{"target": {"name": "FeatureProfileTests"}, "enabled": true}
],
"version": 1
}
Example Apps untuk Development Isolation
Setiap feature module bisa memiliki standalone example app untuk pengembangan yang terisolasi:
swift
// Tuist/ProjectDescriptionHelpers/Target+ExampleApp.swift
import ProjectDescription
public extension Target {
static func exampleApp(
for moduleName: String,
additionalDependencies: [TargetDependency] = []
) -> Target {
.target(
name: "\(moduleName)Example",
destinations: .iOS,
product: .app,
bundleId: "com.company.\(moduleName.lowercased()).example",
deploymentTargets: .iOS("17.0"),
infoPlist: .extendingDefault(with: [
"UILaunchScreen": .dictionary([:]),
"UIMainStoryboardFile": "",
]),
sources: "Example/Sources/**",
resources: [
"Example/Resources/**",
],
dependencies: [
.target(name: moduleName),
] + additionalDependencies
)
}
}
Manfaat example app:
- Developer bisa buka Xcode hanya dengan project satu module — compile time ~30 detik vs seluruh workspace
- SwiftUI previews lebih cepat karena scope kecil
- Isolasi untuk testing edge case tanpa state dari module lain
Testing dengan Mock Injection
swift
// Projects/Core/NetworkKit/Sources/NetworkKit/NetworkClientProtocol.swift
public protocol NetworkClientProtocol: Sendable {
func fetch<T: Decodable>(_ request: URLRequest) async throws -> T
}
swift
// Projects/Core/NetworkKit/Sources/NetworkKit/NetworkClient.swift
public actor NetworkClient: NetworkClientProtocol {
private let session: URLSession
public init(session: URLSession = .shared) {
self.session = session
}
public func fetch<T: Decodable>(_ request: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
return try JSONDecoder().decode(T.self, from: data)
}
}
swift
// Projects/Core/NetworkKit/Tests/NetworkKitTests/MockNetworkClient.swift
@testable import NetworkKit
import Foundation
// Mock yang di-share antar test targets via test dependency
actor MockNetworkClient: NetworkClientProtocol {
private var responses: [String: Any] = [:]
private(set) var requestHistory: [URLRequest] = []
func stub<T: Decodable>(url: String, response: T) {
responses[url] = response
}
func fetch<T: Decodable>(_ request: URLRequest) async throws -> T {
requestHistory.append(request)
guard let url = request.url?.absoluteString,
let response = responses[url] as? T else {
throw NetworkError.invalidResponse
}
return response
}
}