Introduction to Swift Testing Framework

Revolutionising Swift Development: Testing Tools Unveiled at WWDC 2024

Rishita P
Simform Engineering
6 min readJun 18, 2024

--

At WWDC 2024, one of the most exciting tools unveiled was Swift Testing, which makes testing Swift code more powerful than ever. It enables developers to confidently deliver high-quality products with minimal code.

Swift Testing includes innovative features such as built-in support for concurrency, parallel test execution, and the introduction of testing macros.

Table of Contents

Prerequisites

Before using Swift Testing, ensure that you have Xcode 16.0+ and Swift 6.0+

How to add a Swift Testing target?

To begin with Swift Testing, you must select Swift Testing with XCTest UI Tests when creating a new project.

Testing system selection

Now it enables you to write test cases in respective test targets.

Building blocks

It’s essential to make sure your test is readable, especially as your codebase grows more complex. Swift Testing has added some features to help with this, so you will find it easier to write expressive tests.

There are a total of 4 building blocks present. You can find detailed information about each one of them below.

1. Testing functions

The function annotated with @Test indicates a test function. Wherever you use this annotation, Xcode will show you the diamond button over there.

  • It could be a global function or a class method.
  • It could be marked as async or throws.
@Test attribute with a `diamond` button along the side

2. Expectations

  • #expect macro:

It validates whether the expected condition is true or not by providing operators and expressions. Even so, it can handle complex validations.

#expect() success failure scenario

If a failure occurs, detailed information about the failure will be displayed, allowing you to understand where your condition went wrong.

#expect() detailed failure description
  • #expect(.throws: (any Error).self) { } macro:

Instead of writing a catch statement manually, use the expect throws macros which make handling exceptions easier and more efficient.

It will fail if this block does not throw any errors. Otherwise, it will succeed.

APIError and functions utilized for the.expect(throws:) macro

You can have a look at the test case below:

#expect(throws:) { } Success-failure scenario

In the above example, the function that throws an error passed the test successfully, while the function that does not throw any errors failed to pass the test.

  • #require macro:

It can be used if you want to end the test early if an expectation fails. It stops the test if the provided condition is false or unwraps the optional values and stops testing if an expression is found to be nil.

It has a try keyword that throws an error if the expression is false or contains a nil value, stopping further execution.

Macro failure when unwrapping a nil value with #require()

Here, the expectation relies on the required value, which was found to be nil, causing the test to stop execution.

3. Traits

Traits help add descriptive information about tests and allow customization of when or whether a test runs.

Built-in Traits

Choose.tags() over @Test("test name”) to include or exclude the tests.

Use traits wisely, not every situation needs tags. For instance, if you are dealing with runtime conditions, it’s better to use.enable() instead of .tag() .

4. Test Suites

The Test Suites group the test functions and other suites.

The setup and teardown logic are implemented using init() and deinit(), respectively. init() will be called before the test runs and deinit() will be called just after the test runs.

Every test function runs independently on its own instance, so they never share any data by mistake.

Test suite example
Calculator.swift used in TestCalculation.swift

Here, you can see both suites in the test navigator with specified test names. The sub-suite is displayed hierarchically under its parent suite.

Visual representation of the test navigator for Suite

Parameterised testing

Swift Testing offers parameterised tests that allow you to run a single test function with multiple different arguments. This makes testing more efficient and manageable.

Parameterised testing helps ensure thorough test coverage without redundant code or creating an excessive number of individual test cases.

Let’s get some hands-on experience with parameterised tests.

Invalid declaration of parameterised tests

The above indicates the incorrect syntax of parameterised tests in Swift testing. We have to use arguments in the @Test attribute.

Correct declaration of `parameterised tests`

In the above example, three arguments are provided. While two are correct and one is wrong, the overall test case will be marked as failed. However, in the test navigator, you can see which arguments caused the failure.

Test arguments with test results in the test navigator

You can provide additional traits or display names along with arguments.

Parameterised test with additional traits

Positive aspects:

- View the details of each argument in the results.

- Re-execute specific arguments as necessary for debugging the function.

- Tests can be executed more efficiently by running each argument in parallel.

Comparing XCTest and Swift Test

If you are familiar with XCTest, you may be wondering how XCTest differs from Swift Testing.

Comparing Building Blocks in Swift Tests and XCTest:

1. Test functions

Test functions comparison

2. Expectations

Expectations are very different in both frameworks.

XCTest uses specific checks, starting with XCTAssert, to ensure things are working as expected.

However, Swift testing has two basic macros: #expect and #require.

Expectation comparisons

Stopping a test when it fails is also managed differently in both frameworks.

Halt testing approach after failure.

3. Test suites

Test suite Comparisons

Tips for migration from XCTest

  • XCTest and Swift Test can be written in a single target. It is not necessary to create a new target.
  • When multiple methods have a similar structure, you can combine them into one test and make that function parameterised.
  • Combine individual XCTest classes into a single global @Test function .
  • The test prefix in function names is no longer necessary in Swift testing. You can remove it from the functions.
  • Avoid using XCTAssertion in Swift Testing and testing macros like #expect and #require in XCTest.

Continue using XCTest if:

- Tests use a UI automation API like XCUIApplication or a performance metrics API like XCTMetric, which are not supported in Swift testing.

- Tests can only be written in Objective-C.

Conclusion

Swift Testing offers an expressive API with macros, enabling the declaration of test behaviors with minimal code. The #expect API utilizes Swift expressions and operators to validate the provided expressions. Parameterised tests help reduce redundant code by accommodating code with similar structures. Furthermore, Swift Testing supports parallel testing, enhancing overall testing efficiency and productivity. However, if you want to enable serialized testing, you need to use a trait to make it serialized.

For more updates on the latest tools and technologies, follow the Simform Engineering blog.

Follow Us: Twitter | LinkedIn

--

--