ylliX - Online Advertising Network
Swift Parameterized Testing

Swift Parameterized Testing


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:

Test navigator showing value is even test with three passing tests for values 2, 8, and 50

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:

Test navigator showing product is even test with nine passing tests for all combinations of the 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:

Test navigator showing product is even test with three passing tests for the pairs 2 and 3, 8 and 6, and, 50 and 9

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:

Test Navigator showinf five verify string attributes tests, four passing with green ticks and a population test failing with a red cross

Learn More



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *