Migrate an existing test method or test class written using XCTest.
The testing library provides much of the same functionality of XCTest, but uses its own syntax to declare test functions and types. Here, you'll learn how to convert XCTest-based content to use the testing library instead.
XCTest and the testing library are available from different modules. Instead of importing the XCTest module, import the Testing module:
@Row {
@Column {
swift // Before import XCTest
}
@Column {
swift // After import Testing
}
}
A single source file can contain tests written with XCTest as well as other tests written with the testing library. Import both XCTest and Testing if a source file contains mixed test content.
XCTest groups related sets of test methods in test classes: classes that inherit
from the XCTestCase
class provided by the XCTest framework. The testing library doesn't require
that test functions be instance members of types. Instead, they can be free or
global functions, or can be static
or class
members of a type.
If you want to group your test functions together, you can do so by placing them
in a Swift type. The testing library refers to such a type as a suite. These
types do not need to be classes, and they don't inherit from XCTestCase
.
To convert a subclass of XCTestCase
to a suite, remove the XCTestCase
conformance. It's also generally recommended that a Swift structure or actor be
used instead of a class because it allows the Swift compiler to better-enforce
concurrency safety:
@Row {
@Column {
swift // Before class FoodTruckTests: XCTestCase { ... }
}
@Column {
swift // After struct FoodTruckTests { ... }
}
}
For more information about suites and how to declare and customize them, see doc:OrganizingTests.
In XCTest, code can be scheduled to run before and after a test using the
setUp()
and tearDown()
family of functions. When writing tests using the testing library, implement
init()
and/or deinit
instead:
@Row {
@Column {
swift // Before class FoodTruckTests: XCTestCase { var batteryLevel: NSNumber! override func setUp() async throws { batteryLevel = 100 } ... }
}
@Column {
swift // After struct FoodTruckTests { var batteryLevel: NSNumber init() async throws { batteryLevel = 100 } ... }
}
}
The use of async
and throws
is optional. If teardown is needed, declare your
test suite as a class or as an actor rather than as a structure and implement
deinit
:
@Row {
@Column {
swift // Before class FoodTruckTests: XCTestCase { var batteryLevel: NSNumber! override func setUp() async throws { batteryLevel = 100 } override func tearDown() { batteryLevel = 0 // drain the battery } ... }
}
@Column {
swift // After final class FoodTruckTests { var batteryLevel: NSNumber init() async throws { batteryLevel = 100 } deinit { batteryLevel = 0 // drain the battery } ... }
}
}
The testing library represents individual tests as functions, similar to how
they are represented in XCTest. However, the syntax for declaring a test
function is different. In XCTest, a test method must be a member of a test class
and its name must start with test
. The testing library doesn't require a test
function to have any particular name. Instead, it identifies a test function by
the presence of the @Test
attribute:
@Row {
@Column {
swift // Before class FoodTruckTests: XCTestCase { func testEngineWorks() { ... } ... }
}
@Column {
swift // After struct FoodTruckTests { @Test func engineWorks() { ... } ... }
}
}
As with XCTest, the testing library allows test functions to be marked async
,
throws
, or async
-throws
, and to be isolated to a global actor (for example, by using the
@MainActor
attribute.)
- Note: XCTest runs synchronous test methods on the main actor by default, while
the testing library runs all test functions on an arbitrary task. If a test
function must run on the main thread, isolate it to the main actor with
@MainActor
, or run the thread-sensitive code inside a call toMainActor.run(resultType:body:)
.
For more information about test functions and how to declare and customize them, see doc:DefiningTests.
XCTest uses a family of approximately 40 functions to assert test requirements.
These functions are collectively referred to as
XCTAssert()
.
The testing library has two replacements, expect(_:_:sourceLocation:)
and
require(_:_:sourceLocation:)-5l63q
. They both behave similarly to
XCTAssert()
except that require(_:_:sourceLocation:)-5l63q
throws an
error if its condition isn't met:
@Row {
@Column {
swift // Before func testEngineWorks() throws { let engine = FoodTruck.shared.engine XCTAssertNotNil(engine.parts.first) XCTAssertGreaterThan(engine.batteryLevel, 0) try engine.start() XCTAssertTrue(engine.isRunning) }
}
@Column {
swift // After @Test func engineWorks() throws { let engine = FoodTruck.shared.engine try #require(engine.parts.first != nil) #expect(engine.batteryLevel > 0) try engine.start() #expect(engine.isRunning) }
}
}
XCTest also has a function, XCTUnwrap()
,
that tests if an optional value is nil
and throws an error if it is. When
using the testing library, you can use require(_:_:sourceLocation:)-6w9oo
with optional expressions to unwrap them:
@Row {
@Column {
swift // Before func testEngineWorks() throws { let engine = FoodTruck.shared.engine let part = try XCTUnwrap(engine.parts.first) ... }
}
@Column {
swift // After @Test func engineWorks() throws { let engine = FoodTruck.shared.engine let part = try #require(engine.parts.first) ... }
}
}
XCTest has a function, XCTFail()
,
that causes a test to fail immediately and unconditionally. This function is
useful when the syntax of the language prevents the use of an XCTAssert()
function. To record an unconditional issue using the testing library, use the
Issue/record(_:sourceLocation:)
function:
@Row {
@Column {
swift // Before func testEngineWorks() { let engine = FoodTruck.shared.engine guard case .electric = engine else { XCTFail("Engine is not electric") return } ... }
}
@Column {
swift // After @Test func engineWorks() { let engine = FoodTruck.shared.engine guard case .electric = engine else { Issue.record("Engine is not electric") return } ... }
}
}
The following table includes a list of the various XCTAssert()
functions and
their equivalents in the testing library:
XCTest | Swift Testing |
---|---|
XCTAssert(x) , XCTAssertTrue(x) |
#expect(x) |
XCTAssertFalse(x) |
#expect(!x) |
XCTAssertNil(x) |
#expect(x == nil) |
XCTAssertNotNil(x) |
#expect(x != nil) |
XCTAssertEqual(x, y) |
#expect(x == y) |
XCTAssertNotEqual(x, y) |
#expect(x != y) |
XCTAssertIdentical(x, y) |
#expect(x === y) |
XCTAssertNotIdentical(x, y) |
#expect(x !== y) |
XCTAssertGreaterThan(x, y) |
#expect(x > y) |
XCTAssertGreaterThanOrEqual(x, y) |
#expect(x >= y) |
XCTAssertLessThanOrEqual(x, y) |
#expect(x <= y) |
XCTAssertLessThan(x, y) |
#expect(x < y) |
XCTAssertThrowsError(try f()) |
#expect(throws: (any Error).self) { try f() } |
XCTAssertThrowsError(try f()) { error in … } |
let error = #expect(throws: (any Error).self) { try f() } |
XCTAssertNoThrow(try f()) |
#expect(throws: Never.self) { try f() } |
try XCTUnwrap(x) |
try #require(x) |
XCTFail("…") |
Issue.record("…") |
The testing library doesn’t provide an equivalent of
XCTAssertEqual(_:_:accuracy:_:file:line:)
.
To compare two numeric values within a specified accuracy,
use isApproximatelyEqual()
from swift-numerics.
An instance of an XCTestCase
subclass can set its
continueAfterFailure
property to false
to cause a test to stop running after a failure occurs.
XCTest stops an affected test by throwing an Objective-C exception at the
time the failure occurs.
- Note:
continueAfterFailure
isn't fully supported when using the swift-corelibs-xctest library on non-Apple platforms.
The behavior of an exception thrown through a Swift stack frame is undefined. If
an exception is thrown through an async
Swift function, it typically causes
the process to terminate abnormally, preventing other tests from running.
The testing library doesn't use exceptions to stop test functions. Instead, use
the require(_:_:sourceLocation:)-5l63q
macro, which throws a Swift error on
failure:
@Row {
@Column {
swift // Before func testTruck() async { continueAfterFailure = false XCTAssertTrue(FoodTruck.shared.isLicensed) ... }
}
@Column {
swift // After @Test func truck() throws { try #require(FoodTruck.shared.isLicensed) ... }
}
}
When using either continueAfterFailure
or
require(_:_:sourceLocation:)-5l63q
, other tests will continue to run after
the failed test method or test function.
XCTest has a class, XCTestExpectation
,
that represents some asynchronous condition. You create an instance of
this class (or a subclass like XCTKeyPathExpectation
)
using an initializer or a convenience method on XCTestCase
. When the condition
represented by an expectation occurs, the developer fulfills the expectation.
Concurrently, the developer waits for the expectation to be fulfilled using an
instance of XCTWaiter
or using a convenience method on XCTestCase
.
Wherever possible, prefer to use Swift concurrency to validate asynchronous
conditions. For example, if it's necessary to determine the result of an
asynchronous Swift function, it can be awaited with await
. For a function that
takes a completion handler but which doesn't use await
, a Swift
continuation
can be used to convert the call into an async
-compatible one.
Some tests, especially those that test asynchronously-delivered events, cannot
be readily converted to use Swift concurrency. The testing library offers
functionality called confirmations which can be used to implement these tests.
Instances of Confirmation
are created and used within the scope of the
functions confirmation(_:expectedCount:isolation:sourceLocation:_:)-5mqz2
and confirmation(_:expectedCount:isolation:sourceLocation:_:)-l3il
.
Confirmations function similarly to the expectations API of XCTest, however,
they don't block or suspend the caller while waiting for a condition to be
fulfilled. Instead, the requirement is expected to be confirmed (the
equivalent of fulfilling an expectation) before confirmation()
returns, and
records an issue otherwise:
@Row {
@Column {
swift // Before func testTruckEvents() async { let soldFood = expectation(description: "…") FoodTruck.shared.eventHandler = { event in if case .soldFood = event { soldFood.fulfill() } } await Customer().buy(.soup) await fulfillment(of: [soldFood]) ... }
}
@Column {
swift // After @Test func truckEvents() async { await confirmation("…") { soldFood in FoodTruck.shared.eventHandler = { event in if case .soldFood = event { soldFood() } } await Customer().buy(.soup) } ... }
}
}
By default, XCTestExpectation
expects to be fulfilled exactly once, and will
record an issue in the current test if it is not fulfilled or if it is fulfilled
more than once. Confirmation
behaves the same way and expects to be confirmed
exactly once by default. You can configure the number of times an expectation
should be fulfilled by setting its expectedFulfillmentCount
property, and you can pass a value for the expectedCount
argument of
confirmation(_:expectedCount:isolation:sourceLocation:_:)-5mqz2
for the same
purpose.
XCTestExpectation
has a property, assertForOverFulfill
,
which when set to false
allows an expectation to be fulfilled more times than
expected without causing a test failure. When using a confirmation, you can pass
a range to confirmation(_:expectedCount:isolation:sourceLocation:_:)-l3il
as
its expected count to indicate that it must be confirmed at least some number
of times:
@Row {
@Column {
swift // Before func testRegularCustomerOrders() async { let soldFood = expectation(description: "…") soldFood.expectedFulfillmentCount = 10 soldFood.assertForOverFulfill = false FoodTruck.shared.eventHandler = { event in if case .soldFood = event { soldFood.fulfill() } } for customer in regularCustomers() { await customer.buy(customer.regularOrder) } await fulfillment(of: [soldFood]) ... }
}
@Column {
swift // After @Test func regularCustomerOrders() async { await confirmation( "…", expectedCount: 10... ) { soldFood in FoodTruck.shared.eventHandler = { event in if case .soldFood = event { soldFood() } } for customer in regularCustomers() { await customer.buy(customer.regularOrder) } } ... }
}
}
Any range expression with a lower bound (that is, whose type conforms to
both RangeExpression<Int>
and Sequence<Int>
)
can be used with confirmation(_:expectedCount:isolation:sourceLocation:_:)-l3il
.
You must specify a lower bound for the number of confirmations because, without
one, the testing library cannot tell if an issue should be recorded when there
have been zero confirmations.
When using XCTest, the XCTSkip
error type can be thrown to bypass the remainder of a test function. As well,
the XCTSkipIf()
and XCTSkipUnless()
functions can be used to conditionalize the same action. The testing library
allows developers to skip a test function or an entire test suite before it
starts running using the ConditionTrait
trait type. Annotate a test suite or
test function with an instance of this trait type to control whether it runs:
@Row {
@Column {
swift // Before class FoodTruckTests: XCTestCase { func testArepasAreTasty() throws { try XCTSkipIf(CashRegister.isEmpty) try XCTSkipUnless(FoodTruck.sells(.arepas)) ... } ... }
}
@Column {
swift // After @Suite(.disabled(if: CashRegister.isEmpty)) struct FoodTruckTests { @Test(.enabled(if: FoodTruck.sells(.arepas))) func arepasAreTasty() { ... } ... }
}
}
A test may have a known issue that sometimes or always prevents it from passing.
When written using XCTest, such tests can call
XCTExpectFailure(_:options:failingBlock:)
to tell XCTest and its infrastructure that the issue shouldn't cause the test
to fail. The testing library has an equivalent function with synchronous and
asynchronous variants:
withKnownIssue(_:isIntermittent:sourceLocation:_:)
withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:)
This function can be used to annotate a section of a test as having a known issue:
@Row {
@Column {
swift // Before func testGrillWorks() async { XCTExpectFailure("Grill is out of fuel") { try FoodTruck.shared.grill.start() } ... }
}
@Column {
swift // After @Test func grillWorks() async { withKnownIssue("Grill is out of fuel") { try FoodTruck.shared.grill.start() } ... }
}
}
- Note: The XCTest function
XCTExpectFailure(_:options:)
, which doesn't take a closure and which affects the remainder of the test, doesn't have a direct equivalent in the testing library. To mark an entire test as having a known issue, wrap its body in a call towithKnownIssue()
.
If a test may fail intermittently, the call to
XCTExpectFailure(_:options:failingBlock:)
can be marked non-strict. When
using the testing library, specify that the known issue is intermittent
instead:
@Row {
@Column {
swift // Before func testGrillWorks() async { XCTExpectFailure( "Grill may need fuel", options: .nonStrict() ) { try FoodTruck.shared.grill.start() } ... }
}
@Column {
swift // After @Test func grillWorks() async { withKnownIssue( "Grill may need fuel", isIntermittent: true ) { try FoodTruck.shared.grill.start() } ... }
}
}
Additional options can be specified when calling XCTExpectFailure()
:
isEnabled
can be set tofalse
to skip known-issue matching (for instance, if a particular issue only occurs under certain conditions)issueMatcher
can be set to a closure to allow marking only certain issues as known and to allow other issues to be recorded as test failures
The testing library includes overloads of withKnownIssue()
that take
additional arguments with similar behavior:
withKnownIssue(_:isIntermittent:sourceLocation:_:when:matching:)
withKnownIssue(_:isIntermittent:isolation:sourceLocation:_:when:matching:)
To conditionally enable known-issue matching or to match only certain kinds of issues:
@Row {
@Column {
swift // Before func testGrillWorks() async { let options = XCTExpectedFailure.Options() options.isEnabled = FoodTruck.shared.hasGrill options.issueMatcher = { issue in issue.type == thrownError } XCTExpectFailure( "Grill is out of fuel", options: options ) { try FoodTruck.shared.grill.start() } ... }
}
@Column {
swift // After @Test func grillWorks() async { withKnownIssue("Grill is out of fuel") { try FoodTruck.shared.grill.start() } when: { FoodTruck.shared.hasGrill } matching: { issue in issue.error != nil } ... }
}
}
By default, the testing library runs all tests in a suite in parallel. The default behavior of XCTest is to run each test in a suite sequentially. If your tests use shared state such as global variables, you may see unexpected behavior including unreliable test outcomes when you run tests in parallel.
Annotate your test suite with Trait/serialized
to run tests within that
suite serially:
@Row { @Column { ```swift // Before class RefrigeratorTests : XCTestCase { func testLightComesOn() throws { try FoodTruck.shared.refrigerator.openDoor() XCTAssertEqual(FoodTruck.shared.refrigerator.lightState, .on) }
func testLightGoesOut() throws {
try FoodTruck.shared.refrigerator.openDoor()
try FoodTruck.shared.refrigerator.closeDoor()
XCTAssertEqual(FoodTruck.shared.refrigerator.lightState, .off)
}
}
```
} @Column { ```swift // After @Suite(.serialized) class RefrigeratorTests { @Test func lightComesOn() throws { try FoodTruck.shared.refrigerator.openDoor() #expect(FoodTruck.shared.refrigerator.lightState == .on) }
@Test func lightGoesOut() throws {
try FoodTruck.shared.refrigerator.openDoor()
try FoodTruck.shared.refrigerator.closeDoor()
#expect(FoodTruck.shared.refrigerator.lightState == .off)
}
}
```
} }
For more information, see doc:Parallelization.
- doc:DefiningTests
- doc:OrganizingTests
- doc:Expectations
- doc:known-issues