Apple introduced Swift Testing at WWDC24. One of the interesting features is the ability to pass arguments to a test function.
Parameterized Testing
The Swift Testing @Test macro has an arguments parameter which accepts a collection of values:
@Test("Even Value", arguments: [2, 8, 50])
func even(value: Int) {
#expect(value.isMultiple(of: 2))
}
Swift Testing calls the test function once for each value in the arguments collection. The Test Navigator shows the results of each of the test runs:
If you pass a second argument, Swift Testing generates test cases for all combinations of the two arguments:
@Test("Product is even", arguments: [2, 8, 50], [3, 6, 9])
func productEven(value: Int, multiplier: Int) {
let product = value * multiplier
#expect(product.isMultiple(of: 2))
}
The Test Navigator shows nine test results covering all combinations of the input arguments:
You’re limited to at most two arguments. If you don’t need every combination you can zip the arguments to pair them:
@Test("Product is even", arguments: zip([2, 8, 50], [3, 6, 9]))
func productEven(value: Int, multiplier: Int) {
let product = value * multiplier
#expect(product.isMultiple(of: 2))
}
The Test Navigator now shows this test running three times with consecutive pairs of arguments:
That all makes for a good demo but how useful is it in practise?
Migrating from XCTest
I recently migrated some XCTest based unit tests to Swift Testing. I was happy to find examples where I could use parameters to either combine or simplify tests.
The parameter based testing works best when you have a collection of input arguments for which you expect the same result. I’ve found this extra convenient when you can organise the input data as a CaseIterable enum that can drive the tests.
For example, I like to verify the attributes of each property in my Core Data managed object classes. This protects me from accidentally renaming or changing the type of the property in the Core Data model editor. I have the attributes of each Core Data class listed as enums organised by type. For example, a Country
has these String
attributes:
enum StringAttribute: String, CaseIterable {
case capital
case name
case continent
case currency
}
A test to verify each of these string attributes:
private let entityName = "Country"
private let container: NSPeristentContainer
@Test(
"Verify String attributes", arguments: Country.StringAttribute.allCases)
func stringAttributes(_ name: Country.StringAttribute) throws {
let entity = try #require(
container.managedObjectModel.entitiesByName[entityName])
let attribute = try #require(entity.attributesByName[name.rawValue])
#expect(attribute.type == .string)
}
I’m yet to find a practical example for when I need all combinations of two arguments though I have some situations where I’ve found zip’ing two arguments useful.
Why Bother?
I can write the previous test without arguments using a for-loop to iterate over the enum cases. The Swift Testing approach with parameters has some advantages:
Each call of the test function with a different argument is an independent test case than can run in parallel.
It’s much clearer when a test case fails. You can also rerun just the failing argument from the test navigator by clicking on the red failure icon: