Your First Test with @Test
Defining and Validating Behavior
The heart of any unit test is a simple two-step process: performing an action and then validating the outcome. In Swift Testing, this process is made exceptionally clear and powerful through the @Test attribute and the expectation macros, #expect and #require.
Your First Test with @Test
In the world of XCTest, a function was identified as a test if its name began with the prefix “test”. Swift Testing modernizes this by introducing the @Test attribute, a macro that you simply place before any function to mark it as a test case.
This change offers several immediate benefits:
- Flexibility: Your function names are no longer constrained by a naming convention. You can name them descriptively to reflect their purpose.
-
Clarity: The
@Testattribute makes the intent of the function explicit and unambiguous. -
Location Independence: Tests can be global functions; they are no longer required to be methods within a class that inherits from
XCTestCase.
Here is a basic example of a function to be tested and its corresponding test:
// In your main app target
struct TemperatureConverter {
func celsiusToFahrenheit(_ celsius: Double) -> Double {
return (celsius * 9 / 5) + 32
}
}
// In your test target
import Testing
@testable import YourAppName // Allows access to internal types
@Test
func testFreezingPointConversion() {
let converter = TemperatureConverter()
let fahrenheit = converter.celsiusToFahrenheit(0.0)
// We will add a validation in the next step
}
While flexible function names are an improvement, long, descriptive names can sometimes be cumbersome. Swift Testing provides a displayName parameter within the @Test attribute. This allows you to keep your function name concise while providing a more readable, user-facing name that will appear in Xcode’s Test Navigator.
@Test("Verify that 0°C converts to 32°F")
func freezingPoint() {
let converter = TemperatureConverter()
let fahrenheit = converter.celsiusToFahrenheit(0.0)
#expect(fahrenheit == 32.0)
}
In this example, the function is simply named freezingPoint, but in the test report, it will be clearly labeled “Verify that 0°C converts to 32°F.”
Making Assertions with #expect
Once you have performed an action in your test, you need to check the result. This is done using an “expectation.” The primary tool for this in Swift Testing is the #expect macro.
The #expect macro is a significant departure from the large family of XCTAssert functions in XCTest (e.g., XCTAssertEqual, XCTAssertTrue, XCTAssertNil). Instead of learning dozens of different functions, you only need one: #expect. It takes a single argument: any Swift expression that evaluates to a boolean (true or false). If the expression is true, the expectation passes. If it’s false, it fails.
@Test("Verify boiling and body temperature conversions")
func temperatureConversions() {
let converter = TemperatureConverter()
// Check boiling point
let boilingFahrenheit = converter.celsiusToFahrenheit(100.0)
#expect(boilingFahrenheit == 212.0)
// Check approximate human body temperature
let bodyTempFahrenheit = converter.celsiusToFahrenheit(37.0)
#expect(bodyTempFahrenheit == 98.6)
}
The true power of #expect is revealed when an expectation fails. Because it’s a macro, it has access to your source code at compile time. When a test fails, it doesn’t just tell you that a condition was false; it captures the entire expression and the runtime values of its components, presenting a rich, detailed failure message that makes debugging incredibly fast.
For example, if our celsiusToFahrenheit function had a bug and returned 98.5 instead of 98.6, the failure message would look something like this:
✘ Expectation failed: (bodyTempFahrenheit → 98.5) == 98.6
This immediately shows you the value that the variable bodyTempFahrenheit held at the time of the comparison, often eliminating the need to use the debugger to inspect values.
The #expect macro can also be used to verify that a function throws a specific error, a common requirement when testing error-handling logic.
enum PasswordError: Error {
case tooShort
}
func validate(password: String) throws {
if password.count < 8 {
throw PasswordError.tooShort
}
}
@Test("Password validation should throw error for short passwords")
func passwordThrowsError() {
#expect(throws: PasswordError.tooShort) {
try validate(password: "12345")
}
}
Enforcing Preconditions with #require
Sometimes, a test has certain preconditions that must be met for the rest of the test to be meaningful. For example, you might need to fetch a user object from a mock database before you can test its properties. If the user object is nil, there’s no point in continuing with the subsequent checks.
For these situations, Swift Testing provides the #require macro. It is the stricter companion to #expect and has two critical, distinct behaviors :
- Halting Execution: If the condition passed to
#requireevaluates to false, the test stops immediately at that line. This prevents a cascade of confusing, downstream failures that would occur if a fundamental prerequisite is not met. -
Unwrapping Optionals: The
#requiremacro can safely unwrap an optional value. If the optional isnil, the test fails and stops. If it contains a value, that unwrapped value is returned, allowing you to use it in the rest of the test without optional chaining. This is the modern, safer replacement forXCTUnwrap.
Because #require can fail the test by throwing an error, you must call it with the try keyword, and the test function itself must be marked with throws.
Let’s look at an example. Imagine we have a function that finds a video in a library.
struct Video {
let title: String
let duration: Int // in seconds
}
struct VideoLibrary {
private let videos = [
"Intro": Video(title: "Intro", duration: 60),
"Conclusion": Video(title: "Conclusion", duration: 90)
]
func video(withTitle title: String) -> Video? {
return videos[title]
}
}
@Test("Video duration should be 60 seconds")
func videoDurationTest() throws {
let library = VideoLibrary()
// Use #require to fetch and unwrap the video.
// If the video is nil, the test stops here.
let introVideo = try #require(library.video(withTitle: "Intro"))
// Because 'introVideo' is now a non-optional 'Video',
// we can directly access its properties.
#expect(introVideo.duration == 60)
}
In this test, try #require(library.video(withTitle: "Intro")) serves as a crucial guard. If the video for “Intro” isn’t found, the test fails with a clear message, and the #expect line is never reached. This makes the test’s logic cleaner and its failure modes more obvious. Use
#require for essential setup and preconditions, and use #expect for validating the actual outcomes of your test.
To help developers transition from XCTest, the following table maps common XCTest assertions to their modern Swift Testing equivalents.
| XCTest Assertion | Swift Testing Equivalent | Notes |
|---|---|---|
XCTAssertTrue(condition) |
#expect(condition) |
The #expect macro is the universal replacement for boolean checks. |
XCTAssertEqual(a, b) |
#expect(a == b) |
Use standard Swift operators. Failure messages are richer. |
XCTAssertNotEqual(a, b) |
#expect(a!= b) |
Leverage the full power of Swift’s expression language. |
XCTAssertNil(value) |
#expect(value == nil) |
Again, use standard Swift operators for clarity. |
XCTUnwrap(optional) |
let value = try #require(optional) |
#require is safer, stops execution on failure, and returns the unwrapped value. |
XCTAssertThrowsError(...) |
#expect(throws: SomeError.self) {... } |
Swift Testing provides a clear, trailing-closure syntax for error checking. |
This table illustrates a core design principle of the new framework: unifying dozens of specific assertion functions into a single, powerful macro (#expect) that works seamlessly with the Swift language itself.
Now lets look at grouping our tests into Test Suites.