Master Swift Testing framework: features and insights for effective app testing in 2024

Swift, developed by Apple for building applications platforms like iOS and macOS, has evolved into a robust, general-purpose programming language that extends far beyond its initial platform-specific roots.

Despite Swift’s expansion into server-side development, web development, and even machine learning, it has traditionally fallen short in one critical area: testing. The default framework, XCTest, while functional, lacks the flexibility and modern capabilities expected in today’s development. Developers often seek an XCTest alternatives to address issues like outdated testing paradigms and insufficient cross-platform compatibility. Such shortcomings contrast sharply with Swift’s otherwise elegant syntax and usability. To address these gaps, Apple unveiled the Swift Testing framework at WWDC24 to make testing your Swift code a breeze. This new framework leverages Swift’s concurrency and introduces features like macros and parallel testing, making it a to-go framework for efficient and scalable app testing.

How the Swift Testing framework simplifies testing and boosts productivity

Swift Testing leverages modern Swift features, focusing on macros and seamless integration with Swift’s concurrency model. This powerful new testing framework introduces several innovative features designed to simplify and enhance the testing process:

1. Swift Testing macros: create cleaner, more readable test cases

Test functions are marked with the @Test macro, eliminating the need for traditional prefixes like test_. This enables developers to use more descriptive and flexible naming, resulting in cleaner and more readable test code.

2. Automating Setup and Cleanup with Swift Testing framework

Test setup and cleanup are handled automatically using init and deinit, which run before and after each test. This ensures consistent test isolation and improves test maintainability.

3. Parallel testing in Swift for faster execution in large applications

Tests run in parallel by default, significantly reducing execution time and optimising resource usage. This boosts productivity, particularly in large-scale projects.

4. Native support for Swift Concurrency – simplify asynchronous code testing

The framework natively supports asynchronous testing in Swift, simplifying the testing of async/await operations.

5. Maximise test coverage and scalability with parameterised tests in Swift

Developers can define a single test case to run with multiple inputs. This approach enhances test coverage, improves scalability, and reduces repetitive test code.

6. Integrating Swift Testing framework with XCTest

Swift Testing can be used alongside XCTest, enabling a gradual migration to the new framework without disrupting existing test structures.

Swift Testing framework: Practical use cases for real-world scenarios

1. Using the @Test macro for streamlined creation of test cases

By importing the Testing module, developers can utilise the @Test macro to automatically mark functions as test cases, eliminating the need for inheriting from a superclass, as required by XCTest. This approach resolves common challenges such as managing isolation levels and complex setups. Additionally, it removes the need to include the word “test” in function names, resulting in cleaner, more concise, and highly readable code.

struct ProductTests {
  @Test func toyCarTexts() {
    let product = Product.toyCar
    #expect(product.name == "Toy car")
    #expect(product.description == nil)
  }
}

The @Test macro also allows for custom test names, which are reflected in the project navigator for better organization. This streamlined approach helps simplify testing and improves readability.

@Test("Check toy car name and description") func toyCarName() {
	.
}
Example showing the @Test macro in Swift Testing with a custom test name, displayed in the project navigator for improved test organisation and readability.
Example: Project navigator representation

2. Using #expect and #require in Swift Testing

Unlike XCTest, which offers a range of testing APIs (e.g., XCTAssert, XCTAssertEqual, XCTAssertNotNil), Swift Testing introduces two highly flexible and intuitive macros: #expect and #require. Thesemacros take a condition and check whether the executed code satisfies it ornot. Thanks to the power of macros expanding at compile time, the frameworkpreserves expressive feedback when a test fails.

Example use of the #expect macro in Swift Testing to validate conditions for a product

Additionally, the #expect macro includes an extensive set of methods to validate whether the executed code generated the correct result or error.

Using the #expect macro to validate error handling scenarios
Swift Testing example showing the #expect macro to verify login function

Feedback when conditions are not met

Swift Testing example showing the #expect macro to verify login function
Swift Testing example showing the #expect macro to verify login function

The #require macro works similarly to #expect but with an important distinction: it strictly enforces conditions that must be met. If a #require condition fails, the test is immediately halted, and any subsequent steps are skipped. When using #require, the function must be marked as throwing, and calls must be prefixed with try. This macro is especially valuable for working with optional values, as it not only halts the test if the condition isn’t satisfied but also unwraps the optional, eliminating the need to handle potential nil values in subsequent operations.

Using the #require macro in Swift Testing to enforce a mandatory condition

3. Organizing tests with Swift Testing framework’s test suites

Swift Testing provides remarkable flexibility in defining tests. Tests can be written as global functions or within structured types such as structs, classes, or actors. Any type containing functions marked with the @Test macro is automatically treated as a test suite, requiring no additional configuration. For better organisation, test suites can also be nested within one another. This allows developers to break down complex testing scenarios into smaller, more manageable bundles.

Example of using a test suite for password validation

Not only does this help in structuring code, but it is also reflected visually in the project navigator, making it easier to locate and manage test cases.

Structured test suite in Swift Testing framework project navigator

Similar to the @Test macro, test suites in Swift Testing can be customized using display names and traits to control their execution behavior. For example:

  • The .timeLimit trait halts a test if it exceeds a specified TimeInterval, ensuring timely execution.
  • The serialized trait forces tests within a suite to run sequentially instead of in parallel, which is useful for tests that share resources or require strict order.

One significant advantage is that these traits are applied recursively to all nested tests and suites. This ensures consistent behaviour throughout the entire test hierarchy without requiring individual configuration for each test or sub-suite.

For detailed instructions on configuring test traits, refer to Apple’s official Traits in Swift Testing documentation (https://developer.apple.com/documentation/testing/traits).

4. Enhanced testing efficiency with parameterised tests in Swift

The Swift Testing framework streamlines the process of running tests with different inputs through parameterised tests. Instead of duplicating test functions for each scenario, developers can define a single test and provide multiple input values. The framework automatically executes the test for each input, boosting efficiency, minimising code duplication, and significantly increasing test coverage.

For example, consider an extension for a Weekday type with a computed property isWeekend. This property returns true for Saturday and Sunday and false for all other days. Using a parameterised test, developers can test the behaviour for every day of the week by passing each day as an argument, eliminating the need to create separate tests for each case.

Example of parameterised tests in the Swift Testing framework

By structuring tests in this manner, each input is treated as a separate execution of the test function, ensuring comprehensive coverage of all scenarios while keeping the codebase clean and maintainable. This approach reduces maintenance overhead and simplifies adapting tests when new functionality is introduced.

As shown below in the project navigator, parameterised tests appear as individual test cases with their respective results, but they are all managed under a single test function. This organisation enhances scalability and makes the overall testing process more efficient.

Parameterised tests in Swift Testing framework

5. Asynchronous testing made easy: using confirmations in Swift Testing

While Swift concurrency is supported out of the box, real-world applications often encounter more complex asynchronous scenarios, such as handling callbacks. To tackle these challenges, the Swift Testing framework introduces the confirmation function.

This powerful feature enables developers to await specific events and validate their behaviour – whether confirming their occurrence, ensuring they did not occur, or verifying the exact number of times they happened. By providing greater control over asynchronous operations, the confirmation function ensures precise testing and guarantees that systems behave as expected under diverse conditions.

Asynchronous testing with the confirmation function in Swift Testing

Why the Swift Testing framework is the future of scalable application testing

The Swift Testing framework introduces a modern, streamlined approach to testing Swift applications. By leveraging advanced language features such as macros, it eliminates boilerplate code, enhances readability, and simplifies test creation. With native support for parallel execution and seamless integration with Swift’s concurrency model, the framework significantly boosts both performance and efficiency.

Innovative features like parameterised tests optimise test management, ensuring that tests remain clean, maintainable, and scalable as the code evolves. Furthermore, the ability to organise and nest test suites offers flexibility for structuring tests that mirror complex application logic.

A standout advantage of the framework is its compatibility with XCTest, enabling teams to gradually transition without disrupting existing test. This coexistence allows all tests to run under a unified test plan, making the framework a practical choice for both new projects and legacy systems. By improving test coverage, scalability, and reliability, Swift Testing aligns perfectly with Swift 6 principles, empowering developers to build robust and high-quality applications.

Join Mobile Team

Written by
Tobiasz Mateja

No items found.