.agents/skills/swift-testing-expert/references/parameterized-testing.md
Use this file when you have repeated tests with identical logic and only input changes.
for loops with @Test(arguments: ...).// Before: multiple near-duplicate tests.
// @Test func freeFeatureA() { ... }
// @Test func freeFeatureB() { ... }
import Testing
enum Feature: CaseIterable {
case recording, darkMode, networkMonitor
var isPremium: Bool { self == .networkMonitor }
}
@Test("Free features are not premium", arguments: [Feature.recording, .darkMode])
func freeFeatures(_ feature: Feature) {
#expect(feature.isPremium == false)
}
import Testing
func isValidAge(_ value: Int) -> Bool { (18...120).contains(value) }
@Test(arguments: 18...21)
func validAges(_ age: Int) {
#expect(isValidAge(age))
}
zip(...)import Testing
enum Region { case eu, us }
enum Plan { case free, pro }
func canUseVATInvoice(region: Region, plan: Plan) -> Bool {
region == .eu && plan == .pro
}
@Test(arguments: [Region.eu, .us], [Plan.free, .pro])
func vatInvoiceAccess(region: Region, plan: Plan) {
let allowed = canUseVATInvoice(region: region, plan: plan)
#expect((region == .eu && plan == .pro) == allowed)
}
zip for paired scenarioszip when input A must pair with a corresponding input B.zip over full combinations when you need aligned tuples only.zip exampleimport Testing
enum Tier { case basic, premium }
func freeTries(for tier: Tier) -> Int { tier == .basic ? 3 : 10 }
@Test(arguments: zip([Tier.basic, .premium], [3, 10]))
func freeTryLimits(_ tier: Tier, expected: Int) {
#expect(freeTries(for: tier) == expected)
}
zip pitfalls to avoidSilent truncation: zip stops at the shorter collection. If the two arrays differ in length, the extra elements are silently dropped — no compiler error, no test failure, just missing coverage.
// ❌ Silent gap: the fifth input is never tested
@Test(arguments: zip(
[Status.active, .inactive, .pending, .banned, .suspended],
["Active", "Inactive", "Pending", "Banned"] // one short
))
func statusLabel(_ status: Status, expected: String) {
#expect(label(for: status) == expected)
}
Case-order fragility with CaseIterable: pairing two allCases arrays with zip breaks silently if enum cases are ever reordered (e.g., by alphabetizing).
// ❌ Fragile: reordering either enum misaligns all pairs
enum Ingredient: CaseIterable { case rice, potato, egg }
enum Dish: CaseIterable { case onigiri, fries, omelette }
@Test(arguments: zip(Ingredient.allCases, Dish.allCases))
func cook(_ ingredient: Ingredient, into dish: Dish) {
#expect(cook(ingredient) == dish)
}
Prefer explicit array literals or one of the alternatives below.
When inputs and expected outputs must be paired, prefer these over zip to avoid the silent-truncation and case-ordering problems.
Pairs are co-located and impossible to misalign. Adding a new case forces a matching output to be written at the same time.
import Testing
@Test(arguments: [
(Ingredient.rice, Dish.onigiri),
(.potato, .fries),
(.egg, .omelette)
])
func cook(_ ingredient: Ingredient, into dish: Dish) {
#expect(cook(ingredient) == dish)
}
Expresses a clear mapping; each entry is self-documenting. Requires Hashable keys.
import Testing
@Test(arguments: [
Ingredient.rice: Dish.onigiri,
.potato: .fries,
.egg: .omelette
])
func cook(_ ingredient: Ingredient, into dish: Dish) {
#expect(cook(ingredient) == dish)
}
zip with InlineArray (Swift 6.2+)A custom zip overload for InlineArray enforces equal-length arrays at compile time via a generic length parameter. This is not part of the standard library — you must define the helper yourself.
import Testing
// Custom helper: `zip` for two `InlineArray` values of the same length.
func zip<let N: Int, A, B>(
_ a: InlineArray<N, A>,
_ b: InlineArray<N, B>
) -> Zip2Sequence<[A], [B]> {
zip(Array(a), Array(b))
}
// ✅ Compile error if lengths differ — enforced at compile time
@Test(arguments: zip(
InlineArray<2, Ingredient>(.rice, .potato),
InlineArray<2, Dish>(.onigiri, .curry)
))
func cook(_ ingredient: Ingredient, into dish: Dish) {
#expect(cook(ingredient) == dish)
}
CaseIterable.allCases is appropriateUsing allCases as arguments is a valid pattern for property-based tests — tests that verify a universal property holds for every member of a type. The key distinction: the expected result is derived from the property being tested, not from a hard-coded mapping.
import Testing
// ✅ Valid: verifying a mathematical property holds for all orientations.
@Test(
"Rotating clockwise four times returns to the original orientation",
arguments: Orientation.allCases
)
func fullRotation(orientation: Orientation) {
#expect(
orientation
.rotated(.clockwise)
.rotated(.clockwise)
.rotated(.clockwise)
.rotated(.clockwise)
== orientation
)
}
Avoid allCases when you need concrete, case-specific expected values — use explicit arrays or tuples instead.
#expect for case-specific expectations.// ❌ Masking: if format(day) returns "monday" instead of "Monday",
// this test still passes because rawValue has the same casing bug.
@Test(arguments: Day.allCases)
func dayLabel(day: Day) {
#expect(format(day) == day.rawValue)
}
// ✅ Concrete: each expectation is an independent data point.
@Test(arguments: [
(Day.monday, "Monday"),
(.friday, "Friday")
])
func dayLabel(day: Day, expected: String) {
#expect(format(day) == expected)
}
if/switch inside a parameterized test body mirrors implementation logic. Tests that branch the same way as production code verify themselves rather than the behavior independently.// ❌ Mirrors implementation — not independent verification.
@Test(arguments: Day.allCases)
func greeting(day: Day) {
if day == .friday {
#expect(greet(day) == "TGIF!")
} else {
#expect(greet(day) == "Hello, \(day)!")
}
}
// ✅ Separate the special case into its own test.
@Test func fridayGreeting() {
#expect(greet(.friday) == "TGIF!")
}
@Test(arguments: [Day.monday, .tuesday, .wednesday, .thursday, .saturday, .sunday])
func standardGreeting(day: Day) {
#expect(greet(day) == "Hello, \(day)!")
}
for loops instead of parameterized arguments (worse diagnostics).zip with equal-length explicit arrays only when the inputs must remain as separate collections.zip(allCases, allCases).#expect uses concrete literal expectations, not values derived from the input itself.if/switch branching inside parameterized test bodies.CaseIterable.allCases is only used for property-based assertions, not example-based mappings.