Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tuist/tuist/llms.txt
Use this file to discover all available pages before exploring further.
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