Test Automation
Automating tests using the ProjectAutomation framework.Overview
The ProjectAutomation framework enables you to programmatically discover and run tests, making it ideal for custom test runners, CI/CD integration, and selective test execution.Loading the Project Graph
Start by loading the project graph:import ProjectAutomation
let graph = try Tuist.graph()
Discovering Test Targets
Test targets can be identified by their product type:let testTargets = graph.projects.flatMap { project in
project.targets.filter { target in
target.product == "unit_tests" || target.product == "ui_tests"
}
}
Test Target Types
Unit Tests
Targets withproduct == "unit_tests":
let unitTestTargets = graph.projects.flatMap { project in
project.targets.filter { $0.product == "unit_tests" }
}
for target in unitTestTargets {
print("Unit test target: \(target.name)")
print(" Bundle ID: \(target.bundleId)")
print(" Test files: \(target.sources.count)")
}
UI Tests
Targets withproduct == "ui_tests":
let uiTestTargets = graph.projects.flatMap { project in
project.targets.filter { $0.product == "ui_tests" }
}
for target in uiTestTargets {
print("UI test target: \(target.name)")
print(" Bundle ID: \(target.bundleId)")
}
Example: Run All Tests
import Foundation
import ProjectAutomation
@main
struct TestRunner {
static func main() throws {
let graph = try Tuist.graph()
print("Discovering test targets...\n")
// Find all test targets
let testTargets = graph.projects.flatMap { project -> [(String, Target)] in
project.targets
.filter { $0.product == "unit_tests" || $0.product == "ui_tests" }
.map { (project.name, $0) }
}
print("Found \(testTargets.count) test targets\n")
var results: [TestResult] = []
// Run each test target
for (projectName, target) in testTargets {
print("Running \(target.name) in \(projectName)...")
let result = try runTests(target: target.name)
results.append(result)
if result.passed {
print("✓ \(target.name): Passed\n")
} else {
print("✗ \(target.name): Failed\n")
}
}
// Print summary
printSummary(results)
}
static func runTests(target: String) throws -> TestResult {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcodebuild")
process.arguments = [
"test",
"-scheme", target,
"-destination", "platform=iOS Simulator,name=iPhone 15",
"-enableCodeCoverage", "YES"
]
try process.run()
process.waitUntilExit()
return TestResult(
target: target,
passed: process.terminationStatus == 0
)
}
static func printSummary(_ results: [TestResult]) {
let passed = results.filter { $0.passed }.count
let failed = results.count - passed
print("\n" + String(repeating: "=", count: 50))
print("Test Summary")
print(String(repeating: "=", count: 50))
print("Total: \(results.count)")
print("Passed: \(passed)")
print("Failed: \(failed)")
if failed > 0 {
print("\nFailed targets:")
for result in results where !result.passed {
print(" - \(result.target)")
}
}
}
struct TestResult {
let target: String
let passed: Bool
}
}
Example: Selective Test Execution
Run only specific tests based on criteria:import Foundation
import ProjectAutomation
@main
struct SelectiveTestRunner {
static func main() throws {
let graph = try Tuist.graph()
// Get test filter from environment
let testFilter = ProcessInfo.processInfo.environment["TEST_FILTER"] ?? "unit"
print("Test Filter: \(testFilter)\n")
let allTests = graph.projects.flatMap { project -> [(String, Target)] in
project.targets
.filter { $0.product == "unit_tests" || $0.product == "ui_tests" }
.map { (project.name, $0) }
}
// Filter based on criteria
let testsToRun = allTests.filter { (_, target) in
switch testFilter {
case "unit":
return target.product == "unit_tests"
case "ui":
return target.product == "ui_tests"
case "all":
return true
default:
return target.name.contains(testFilter)
}
}
print("Running \(testsToRun.count) test targets\n")
for (projectName, target) in testsToRun {
print("Testing \(target.name) from \(projectName)...")
try runTest(target: target)
}
}
static func runTest(target: Target) throws {
print(" Product: \(target.product)")
print(" Sources: \(target.sources.count) files")
print(" Dependencies: \(target.dependencies.count)")
// Execute test
}
}
Example: Test Dependency Analysis
Analyze test target dependencies:import Foundation
import ProjectAutomation
@main
struct TestDependencyAnalyzer {
static func main() throws {
let graph = try Tuist.graph()
print("Test Dependency Analysis\n")
print(String(repeating: "=", count: 60))
for project in graph.projects {
let testTargets = project.targets.filter {
$0.product == "unit_tests" || $0.product == "ui_tests"
}
guard !testTargets.isEmpty else { continue }
print("\nProject: \(project.name)")
print("Path: \(project.path)\n")
for target in testTargets {
print(" \(target.name) (\(target.product))")
print(" Bundle ID: \(target.bundleId)")
print(" Test Files: \(target.sources.count)")
print(" Dependencies:")
if target.dependencies.isEmpty {
print(" (none)")
} else {
for dependency in target.dependencies {
switch dependency {
case .target(let name):
print(" - Target: \(name)")
case .external(let name):
print(" - External: \(name)")
case .framework(let path):
print(" - Framework: \(path)")
case .sdk(let name, _):
print(" - SDK: \(name)")
case .xctest:
print(" - XCTest")
default:
print(" - Other")
}
}
}
print()
}
}
}
}
Example: Code Coverage Analysis
Run tests with code coverage:import Foundation
import ProjectAutomation
@main
struct CoverageRunner {
static func main() throws {
let graph = try Tuist.graph()
let coverageTargets = ProcessInfo.processInfo.environment["COVERAGE_TARGETS"]?.split(separator: ",").map(String.init) ?? []
print("Running tests with code coverage\n")
// Find unit test targets
let unitTests = graph.projects.flatMap { project in
project.targets.filter { $0.product == "unit_tests" }
}
for testTarget in unitTests {
print("Testing: \(testTarget.name)")
// Determine coverage targets
let targets = coverageTargets.isEmpty ?
findCoverageTargets(for: testTarget) :
coverageTargets
print(" Coverage targets: \(targets.joined(separator: ", "))")
try runTestsWithCoverage(
testTarget: testTarget.name,
coverageTargets: targets
)
}
}
static func findCoverageTargets(for testTarget: Target) -> [String] {
// Analyze dependencies to find targets to measure coverage for
testTarget.dependencies.compactMap { dependency in
switch dependency {
case .target(let name):
return name
default:
return nil
}
}
}
static func runTestsWithCoverage(testTarget: String, coverageTargets: [String]) throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/xcodebuild")
var arguments = [
"test",
"-scheme", testTarget,
"-destination", "platform=iOS Simulator,name=iPhone 15",
"-enableCodeCoverage", "YES"
]
// Add coverage targets
for target in coverageTargets {
arguments.append(contentsOf: ["-only-testing", target])
}
process.arguments = arguments
try process.run()
process.waitUntilExit()
if process.terminationStatus == 0 {
print(" ✓ Tests passed with coverage\n")
} else {
print(" ✗ Tests failed\n")
}
}
}
Example: Parallel Test Execution
Run tests in parallel for faster execution:import Foundation
import ProjectAutomation
@main
struct ParallelTestRunner {
static func main() async throws {
let graph = try Tuist.graph()
let testTargets = graph.projects.flatMap { project in
project.targets.filter { $0.product == "unit_tests" }
}
print("Running \(testTargets.count) test targets in parallel\n")
// Run tests in parallel using async/await
await withTaskGroup(of: TestResult.self) { group in
for target in testTargets {
group.addTask {
do {
try await runTestAsync(target: target.name)
return TestResult(target: target.name, passed: true)
} catch {
return TestResult(target: target.name, passed: false)
}
}
}
var results: [TestResult] = []
for await result in group {
results.append(result)
let status = result.passed ? "✓" : "✗"
print("\(status) \(result.target)")
}
printSummary(results)
}
}
static func runTestAsync(target: String) async throws {
// Async test execution
}
static func printSummary(_ results: [TestResult]) {
let passed = results.filter { $0.passed }.count
print("\nSummary: \(passed)/\(results.count) passed")
}
struct TestResult {
let target: String
let passed: Bool
}
}
Best Practices
- Parallel Execution: Run independent tests in parallel to reduce total test time.
- Selective Testing: Filter tests based on changes, affected targets, or test type.
- Code Coverage: Enable code coverage for unit tests to track test quality.
- Retry Logic: Implement retry logic for flaky tests.
- Result Reporting: Generate detailed test reports for CI/CD integration.
- Test Isolation: Ensure tests are isolated and can run independently.
Related APIs
- ProjectAutomation Overview - Framework overview
- Build Automation - Build automation
- Target API - Target manifest definition