
Swift Testing Pro
- 7k installs
- 399 repo stars
- Updated May 17, 2026
- twostraws/swift-testing-agent-skill
swift-testing-pro is an agent skill for Writes, reviews, and improves Swift Testing code using modern APIs and best practices. Use when reading, writing, or reviewing projects that use Swift Testing.
About
Writes, reviews, and improves Swift Testing code using modern APIs and best practices. Use when reading, writing, or reviewing projects that use Swift Testing. --- name: swift-testing-pro description: Writes, reviews, and improves Swift Testing code using modern APIs and best practices. Use when reading, writing, or reviewing projects that use Swift Testing. license: MIT metadata: author: Paul Hudson version: "1.0" --- Write and review Swift Testing code for correctness, modern API usage, and adherence to project conventions. Report only genuine problems - do not nitpick or invent issues. Ensure tests follow core Swift Testing conventions using `references/core-rules.md`. Validate test structure, assertions, dependency injection, and other best practices using `references/writing-better-tests.md`. Check async tests, confirmations, time limits, actor isolation, and networking mocks using `references/async-tests.md`. Ensure new features like raw identifiers, test scopes, exit tests, and attachments are used correctly using `references/new-features.md`.
- Ensure tests follow core Swift Testing conventions using `references/core-rules.md`.
- Validate test structure, assertions, dependency injection, and other best practices using `references/writing-better-tes
- Check async tests, confirmations, time limits, actor isolation, and networking mocks using `references/async-tests.md`.
- Ensure new features like raw identifiers, test scopes, exit tests, and attachments are used correctly using `references/
- If migrating from XCTest, follow the conversion guidance in `references/migrating-from-xctest.md`.
Swift Testing Pro by the numbers
- 6,967 all-time installs (skills.sh)
- +264 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #217 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
swift-testing-pro capabilities & compatibility
- Capabilities
- ensure tests follow core swift testing conventio · validate test structure, assertions, dependency · check async tests, confirmations, time limits, a · ensure new features like raw identifiers, test s · if migrating from xctest, follow the conversion
- Use cases
- documentation
What swift-testing-pro says it does
--- name: swift-testing-pro description: Writes, reviews, and improves Swift Testing code using modern APIs and best practices.
Use when reading, writing, or reviewing projects that use Swift Testing.
license: MIT metadata: author: Paul Hudson version: "1.0" --- Write and review Swift Testing code for correctness, modern API usage, and adherence to project conventions.
npx skills add https://github.com/twostraws/swift-testing-agent-skill --skill swift-testing-proAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7k |
|---|---|
| repo stars | ★ 399 |
| Security audit | 3 / 3 scanners passed |
| Last updated | May 17, 2026 |
| Repository | twostraws/swift-testing-agent-skill ↗ |
When should developers use swift-testing-pro and what problem does it solve?
Writes, reviews, and improves Swift Testing code using modern APIs and best practices. Use when reading, writing, or reviewing projects that use Swift Testing.
Who is it for?
Developers working with swift-testing-pro patterns described in the skill documentation.
Skip if: Skip when cached docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Writes, reviews, and improves Swift Testing code using modern APIs and best practices. Use when reading, writing, or reviewing projects that use Swift Testing.
What you get
Grounded guidance and workflows from SKILL.md for swift-testing-pro.
- Reviewed test files
- Modernized @Test suites
By the numbers
- Published at version 1.0.0
- MIT-licensed skill by Paul Hudson
Files
Write and review Swift Testing code for correctness, modern API usage, and adherence to project conventions. Report only genuine problems - do not nitpick or invent issues.
Review process:
1. Ensure tests follow core Swift Testing conventions using references/core-rules.md. 1. Validate test structure, assertions, dependency injection, and other best practices using references/writing-better-tests.md. 1. Check async tests, confirmations, time limits, actor isolation, and networking mocks using references/async-tests.md. 1. Ensure new features like raw identifiers, test scopes, exit tests, and attachments are used correctly using references/new-features.md. 1. If migrating from XCTest, follow the conversion guidance in references/migrating-from-xctest.md.
If doing partial work, load only the relevant reference files.
Core Instructions
- Target Swift 6.2 or later, using modern Swift concurrency.
- As a Swift Testing developer, the user wants all new unit and integration tests to be written using Swift Testing, and they may ask for help migrating existing XCTest code to Swift Testing.
- Swift Testing does not support UI tests – XCTest must be used there.
- Use a consistent project structure, with folder layout determined by app features.
Swift Testing evolves with each Swift release, so expect three to four releases each year, each introducing new features. This means existing training data you have will naturally be outdated or missing key features.
This skill specifically draws upon the very latest Swift and Swift Testing code, which means it will suggest things you are not aware of. Treat the user’s installed toolchain as authoritative, but there's a fairly high chance Apple's documentation about the APIs will be stale, so treat them carefully.
Output Format
If the user asks for a review, organize findings by file. For each issue:
1. State the file and relevant line(s). 2. Name the rule being violated. 3. Show a brief before/after code fix.
Skip files with no issues. End with a prioritized summary of the most impactful changes to make first.
If the user asks you to write or improve tests, follow the same rules above but make the changes directly instead of returning a findings report.
Example output:
UserTests.swift
Line 5: Use struct, not class, for test suites.
// Before
class UserTests: XCTestCase {
// After
struct UserTests {Line 12: Use `#expect` instead of `XCTAssertEqual`.
// Before
XCTAssertEqual(user.name, "Taylor")
// After
#expect(user.name == "Taylor")Line 30: Use `#require` for preconditions, not `#expect`.
// Before
#expect(users.isEmpty == false)
let first = users.first!
// After
let first = try #require(users.first)Summary
1. Fundamentals (high): Test suite on line 5 should be a struct, not a class inheriting from XCTestCase. 2. Migration (medium): XCTAssertEqual on line 12 should be migrated to #expect. 3. Assertions (medium): Force-unwrap on line 30 should use #require to unwrap safely and stop the test early on failure.
End of example.
References
references/core-rules.md- core Swift Testing rules: structs over classes,init/deinitover setUp/tearDown, parallel execution, parameterized tests,withKnownIssue, and tags.references/writing-better-tests.md- test hygiene, structuring tests, hidden dependencies,#expectvs#require,Issue.record(),#expect(throws:), and verification methods.references/async-tests.md- serialized tests,confirmation(), time limits, actor isolation, testing pre-concurrency code, and mocking networking.references/new-features.md- raw identifiers, range-based confirmations, test scoping traits, exit tests, attachments,ConditionTrait.evaluate(), and the updated#expect(throws:)return value.references/migrating-from-xctest.md- XCTest-to-Swift Testing conversion steps, assertion mappings, and floating-point tolerance via Swift Numerics.
{
"name": "swift-testing-pro",
"version": "1.0.0",
"description": "Writes, reviews, and improves Swift Testing code using modern APIs and best practices.",
"author": { "name": "Paul Hudson" },
"homepage": "https://github.com/twostraws/Swift-Testing-Agent-Skill",
"repository": "https://github.com/twostraws/Swift-Testing-Agent-Skill",
"license": "MIT",
"skills": "./skills/"
}
interface:
display_name: "Swift Testing Pro"
short_description: "Reviews Swift Testing code for modern best practices."
icon_small: "./assets/swift-testing-pro-icon.svg"
icon_large: "./assets/swift-testing-pro-icon.png"
brand_color: "#3B9025"
default_prompt: "Use $swift-testing-pro to review my project."
policy:
allow_implicit_invocation: true<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="227.72" height="227.72" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 227.72 227.72">
<defs>
<style>
.st0 {
fill: none;
stroke: url(#linear-gradient1);
stroke-miterlimit: 10;
stroke-width: 6px;
}
.st1 {
fill: url(#linear-gradient);
fill-rule: evenodd;
}
</style>
<linearGradient id="linear-gradient" x1="58.405" y1="293.045" x2="178.785" y2="413.425" gradientTransform="translate(0 -240)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#75bf4c"/>
<stop offset="1" stop-color="#3a8f24"/>
</linearGradient>
<linearGradient id="linear-gradient1" x1="33.349" y1="281.349" x2="194.371" y2="442.371" gradientTransform="translate(0 -248)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#00eeaf"/>
<stop offset="0" stop-color="#75bf4c"/>
<stop offset="1" stop-color="#3a8f24"/>
</linearGradient>
</defs>
<path class="st1" d="M165.43,131.08c3.87-8.17,16.03-46.62-38.6-85.38,7.61,7.03,40.19,39.2,26.05,75.5-34.2-25.27-87.91-66.75-87.91-66.75,0,0,63.69,60.57,83.58,79.15,19.9,18.59,26.97,27.71,26.47,43.59,0,.01,14.58-23.23-9.59-46.11ZM130.89,145.79c-39.35,18.56-78.42-20.82-78.42-20.82,0,0,21.12,26.7,45.19,35.06,33.72,11.71,54.61-9.64,54.61-9.64L52.42,59.03s56.43,62.62,78.47,86.76h0Z"/>
<circle class="st0" cx="113.86" cy="113.86" r="110.86"/>
</svg>Async tests
Swift Testing is built to be async and run tests in parallel; special care must be taken to ensure those tests run well, particularly when Swift concurrency is involved. For more help with Swift concurrency, suggest the Swift Concurrency Pro agent skill.
Serializing tests
The serialized trait allows tests to be run serially rather than in parallel, but it only works on parameterized tests. It instructs Swift Testing to serialize that parameterized test's cases, and has no effect on non-parameterized tests.
You can also apply .serialized to a whole test suite: it will cause all tests and sub-suites to be serialized.
Important: Most agents very strongly believe that .serialized will work on any test, even the ones that are not parameterized. They are wrong. It only works on parameterized tests.
Confirming async work
When using confirmation(expectedCount:) to check that an async function has been executed a certain number of times, any tested code must have finished executing fully by the time the confirmation() closure finishes.
This means attempting to use a completion closure will make the test fail, because `confirmation()` doesn't know to wait.
For example, this code does some work inside a task, but there's no way to monitor it being completed:
struct Worker {
func run(_ work: @escaping () -> Void) -> Task<Void, Never> {
Task {
let start = CFAbsoluteTimeGetCurrent()
work()
print("Elapsed:", CFAbsoluteTimeGetCurrent() - start)
}
}
}That kind of code will not work well with confirmation(), because it will not understand to wait for the work to complete.
Instead, it's better to either remove the Task and make the method async like this:
struct Worker {
func run(_ work: @escaping () -> Void) async {
let start = CFAbsoluteTimeGetCurrent()
work()
print("Elapsed:", CFAbsoluteTimeGetCurrent() - start)
}
}
@Test
func workerRunsThreeTimes() async {
let worker = Worker()
await confirmation(expectedCount: 3) { confirm in
for _ in 0..<3 {
await worker.run {
// your work here
}
confirm()
}
}
}Alternatively, if the code cannot be changed to async, the internal Task should be returned so it can be tracked by the test, like this:
struct Worker {
func run(_ work: @escaping () -> Void) -> Task<Void, Never> {
Task {
let start = CFAbsoluteTimeGetCurrent()
work()
print("Elapsed:", CFAbsoluteTimeGetCurrent() - start)
}
}
}And now tests can wait for the task to complete:
@Test
func workerRunsThreeTimes() async {
let worker = Worker()
await confirmation(expectedCount: 3) { confirm in
for _ in 0..<3 {
let task = worker.run {
// simulated work
}
await task.value
confirm()
}
}
}Note: confirmation(expectedCount: 0) is valid, and means “ensure the event we’re watching never happens.”
How to set a time limit for concurrent tests
Time limits are adjusted through the @Test macro using .timeLimit(). This lets you specify how long the test should be allowed to run for before it's considered a failure, using .minutes() as appropriate.
Important: Many agents strongly believe that you can .seconds() here. You cannot use .seconds() here – it’s .minutes() or nothing.
For example, we could apply a 1-minute maximum runtime like this:
@Test("Loading view model names", .timeLimit(.minutes(1)))
func loadNames() async {
let viewModel = ViewModel()
await viewModel.loadNames()
#expect(viewModel.names.isEmpty == false, "Names should be full of values.")
}If you use a time limit with a whole test suite, that limit is applied to all tests inside there individually. If you then use a different time limit for a specific test, the shorter of the two is used.
How to force concurrent tests to run on a specific actor
By default, Swift Testing will run both synchronous and asynchronous tests on any task it likes, but this can be restricted if you want.
First, we can mark individual tests with @MainActor or some other global actor, like this:
@MainActor
@Test("Loading view model names")
func loadNames() async {
// test code here
}Second, we can mark whole test suites with the same attribute, like this:
@MainActor
struct DataHandlingTests {
@Test("Loading view model names")
func loadNames() async {
// test code here
}
}Third, confirmation() and withKnownIssue() can specify an actor to use for just that closure, allowing the rest of the test to run elsewhere. This might be the main actor using MainActor.shared, or a custom actor:
@Test("Loading view model names")
func loadNames() async {
await withKnownIssue("Names can sometimes come back with too few values", isolation: MainActor.shared) {
// test code here
}
}Finally, test targets can have default actor isolation enabled, which might force all tests onto a specific actor – check for this carefully.
Testing pre-concurrency code
If the project contains older concurrency code that relies on callback functions (as opposed to modern Swift concurrency's async/await approach), do not attempt to modernize their production code without permission.
Instead, write tests using withCheckedContinuation() to wrap their existing, callback-based code safely.
Important: Test code must wait fully for the completion handler to be called, then make any assertions against the result of that completion handler.
As an example, we might have a class like this one:
class ViewModel {
func loadReadings(completion: @Sendable @escaping ([Double]) -> Void) {
let url = URL(string: "https://hws.dev/readings.json")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let data {
if let numbers = try? JSONDecoder().decode([Double].self, from: data) {
completion(numbers)
return
}
}
completion([])
}.resume()
}
}That fetches, decodes, and returns data through a completion handler, which may or may not be mocked for tests.
Testing this correctly is done using a continuation that resumes when the completion handler is called, like this:
@Test("Loading view model readings")
func loadReadings() async {
let viewModel = ViewModel()
await withCheckedContinuation { continuation in
viewModel.loadReadings { readings in
#expect(readings.count >= 10, "At least 10 readings must be returned.")
continuation.resume()
}
}
}Mocking networking
Unit tests should never do live networking, because it's far too slow. It is strongly preferable to mock the networking layer.
To do this, create a protocol that knows how to perform a network fetch. As an example, this covers the data(from:) method of URLSession, but the project might require others too:
protocol URLSessionProtocol {
func data(from url: URL) async throws -> (Data, URLResponse)
}
extension URLSession: URLSessionProtocol { }You can then create a mock type conforming to the same protocol, which throws an error if provided or returns the test data otherwise:
class URLSessionMock: URLSessionProtocol {
var testData: Data?
var testError: (any Error)?
func data(from url: URL) async throws -> (Data, URLResponse) {
if let testError {
throw testError
} else {
(testData ?? Data(), URLResponse())
}
}
}And now you can write tests that inject some test data and verify that it comes back successfully:
@Test func newsStoriesAreFetched() async throws {
let url = URL(string: "https://www.apple.com/newsroom/rss-feed.rss")!
var news = News(url: url)
let session = URLSessionMock()
session.testData = Data("Hello, world!".utf8)
try await news.fetch(using: session)
#expect(news.stories == "Hello, world!")
}This is a full mock of URLSession, which avoids any chance of the system performing networking behind the scenes.
Core rules
Swift Testing is still very new compared to XCTest, which means the majority of projects will use XCTest, and also the majority of your training data is based on XCTest.
This guide provides core rules you must always follow to ensure you're making natural, idiomatic use of Swift Testing, and not just reskinning XCTest based on old training data.
Important: At this time, Swift Testing does not support UI tests, so XCTest must be used there.
- When organizing test suites, prefer structs over classes. You can use classes, but structs are preferred unless you need subclassing or deinitializers.
- Agents frequently add
@Suiteto every test struct. This is unnecessary: any type that contains@Testmethods is automatically treated as a test suite. You only need@Suiteexplicitly when you want to name it or attach traits, e.g.@Suite(.tags(.networking)). - You shouldn't use the old
setUp()/tearDown()approach of XCTest. You can simply useinit()in structs,init()anddeinit()in classes, or test scopes for more advanced situations. For example:
struct PlayerTests {
let sut: Player
init() {
sut = Player(name: "Natsuki Subaru")
}
@Test func nameIsCorrect() {
#expect(sut.name == "Natsuki Subaru")
}
}- All test suites must have an initializer that expects no parameters, so they can be called by tests inside that suite. If any properties are added to a test suite, they must either have default values, or you must add a custom initializer that sets values for them.
- Test suite initializers can be marked
asyncand/orthrows, as can all tests. - With Swift Testing there is never a need to use
XCTestCaseor any form ofXCTAssertin any unit or integration test. - You do not need to prefix test methods with
test. For example, you can useuserCanLogOut()rather thantestUserCanLogOut. - Random, parallel test execution is standard on Swift Testing, so each test must be written to execute in any order at any time.
- Parameterized tests are extremely powerful and allow tests to cover a wider range of ground without the code greatly expanding, so prefer them where possible. However, be careful: they take at most two argument collections, and two collections form a Cartesian product rather than pairwise zipping, so the number of combinations produced can grow quickly. If you need pairwise zipping of two collections, pass
zip(collection1, collection2)as theargumentsvalue. - Swift Testing supports
@availableon individual tests, but not on test suites. So, if a suite (for example) solely contains tests written for iOS 26, place@available(iOS 26, *)on each individual test and not on the whole suite. - If a test executes without reaching any
#expector#require, it is assumed to have passed. - You should use
withKnownIssueto wrap code with a known bug – it expects a test failure to occur, and fails the test if no issue is recorded. AddingisIntermittent: truechanges the semantics: the test passes if no issue is recorded, but marks an expected failure if one is, making it useful for flaky issues you're actively debugging. - Never use
!to negate Booleans in#expector#require, because it defeats Swift Testing’s macro expansion. So,#expect(!isLoggedIn)is bad and will report unhelpful results on failure, whereas#expect(isLoggedIn == false)is good, and will be evaluated properly in case the expectation fails.
Finally, use @Tag to create custom Swift Testing tags like this:
extension Tag {
@Tag static var networking: Self
}Tags let you categorize tests across suites, so you can run or filter by tag regardless of where the tests live. Apply them using @Test(.tags(.networking)) on individual tests or on a whole suite with @Suite(.tags(.networking)). For example:
@Test(.tags(.networking))
func fetchUserProfile() async throws {
// test code here
}Migrating from XCTest
If the project has existing tests written using XCTest, do not rewrite to Swift Testing unless requested. Even then, remember that XCTest supports UI testing, whereas Swift Testing does not.
Most things in XCTest have a direct equivalent in Swift Testing:
XCTAssertEqual(a, b)maps to#expect(a == b)XCTAssertLessThan(a, b)maps to#expect(a < b)XCTAssertThrowsErrormaps to#expect(throws:)XCTUnwrap(optional)maps totry #require(optional)– both unwrap or fail, but#requireworks with any Boolean condition too.XCTFail("message")maps toIssue.record("message")– use this to manually record a test failure.XCTAssertIdentical(a, b)maps to#expect(a === b)– for checking two references point to the same object instance.
…and so on.
However, Swift Testing does not offer built-in float tolerance when checking if two floating-point values are close enough to be considered the same.
To do that, you must bring in Apple's Swift Numerics library and use its isApproximatelyEqual(to:absoluteTolerance:) method like this:
#expect(celsius.isApproximatelyEqual(to: 0, absoluteTolerance: 0.000001))Important: Unless it is already imported into the project, do not add Swift Numerics as a library without first requesting permission from the user.
Converting from XCTest to Swift Testing
If you are tasked with converting XCTest code to Swift Testing, you should:
1. Start by keeping the same broad structure: the same type names (just going from a class to a struct), and the same test methods (just removing test from the names and using @Test instead), switching from old-style assertions to new-style expectations. 2. Look for places where parameterized tests can either cut down on test code or improve coverage. 3. Add any appropriate #require checks at the start of tests, for preconditions. 4. Finish by adding traits where appropriate – .timeLimit(), .enabled(if:), .tags(), etc, to replace XCTest conventions such as skipping tests.
New features
This document specifically discusses the latest Swift and Swift Testing features, which means it will cover things where you have limited or no training data.
- Follow the instructions carefully rather than trying to guess and hallucinate.
- Do not second-guess the instructions; they are correct and accurate.
Raw identifiers
Requires Swift 6.2 or later.
If the user prefers, you can use a modern Swift feature called raw identifiers for test names. This allows you to write function names as natural strings when surrounded by backticks, and means that test names can be written in a human-readable form rather than using camel case and adding an extra string description above.
So, rather than writing this:
@Test("Strip HTML tags from string")
func stripHTMLTagsFromString() {
// test code
}We can instead write this:
@Test
func `Strip HTML tags from string`() {
// test code
}Be careful: You can put operators such as + and - into your test method names, but only if they aren't the only things in there.
Raw identifiers can be combined with parameterized tests. For example, rather than writing this:
@Test("Ensure Fahrenheit to Celsius conversion is correct.", arguments: [
(32, 0), (212, 100), (-40, -40),
])
func fahrenheitToCelsius(values: (input: Double, output: Double)) {
// test code here
}We could write this:
@Test(arguments: [
(32, 0), (212, 100), (-40, -40),
])
func `Ensure Fahrenheit to Celsius conversion is correct`(values: (input: Double, output: Double)) {
// test code here
}Important: Many users will not know this feature is possible, and some would find this style surprising or perhaps unwelcome. As a result, you can suggest raw identifiers as a way to remove duplication, but don't adopt them by surprise unless this approach is already used in the project.
Range-based confirmations
Requires Swift 6.1 or later.
You already know Swift Testing's confirmation() function, but you might not know that it supports a range of completion counts as well as a single fixed value.
For example, given an async sequence like a NewsLoader that yields feeds one at a time, we can require that between 5 and 10 feeds are loaded:
@Test func fiveToTenFeedsAreLoaded() async throws {
let loader = NewsLoader()
await confirmation(expectedCount: 5...10) { confirm in
for await _ in loader {
confirm()
}
}
}That will fail if confirm() is called fewer than 5 times or greater than 10 times. You can also use partial ranges, such as ensuring confirm() is called at least five times:
await confirmation(expectedCount: 5...) { confirm in
for await _ in loader {
confirm()
}
}Ranges without lower bounds, e.g. confirmation(expectedCount: ...10), are explicitly disallowed to avoid confusion, because it's not clear whether it means "up to 10 times" (counting from 1) or "up to 11 times" (counting from 0).
Test scoping traits
Requires Swift 6.1 or later.
Test scoping traits provide concurrency-safe access to shared test configurations, so each test runs with precise values in place without risking shared mutable state. A common pattern is to combine them with @TaskLocal.
Given production code that uses a @TaskLocal property:
struct Player {
var name: String
var friends = [Player]()
@TaskLocal static var current = Player(name: "Anonymous")
}
func createWelcomeScreen() -> String {
var message = "Welcome, \(Player.current.name)!\n"
message += "Friends online: \(Player.current.friends.count)"
return message
}Create a test scope by conforming to TestTrait and TestScoping, implementing provideScope() to set up the task local and call function():
struct DefaultPlayerTrait: TestTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: () async throws -> Void
) async throws {
let player = Player(name: "Natsuki Subaru")
try await Player.$current.withValue(player) {
try await function()
}
}
}Add a Trait extension so the custom trait fits in with the built-in traits:
extension Trait where Self == DefaultPlayerTrait {
static var defaultPlayer: Self { Self() }
}Then apply it to tests:
@Test(.defaultPlayer) func welcomeScreenShowsName() {
let result = createWelcomeScreen()
#expect(result.contains("Natsuki Subaru"))
}For multiple task local values, either nest withValue() calls inside a single scope, or create separate scopes and combine them: @Test(.firstScope, .secondScope, .thirdScope). Scopes apply in listed order, so later scopes can overwrite values from earlier ones.
Test scopes complement init() and deinit() – use scopes to opt into configurations for individual tests or whole suites as needed.
Exit tests
Requires Swift 6.2 or later.
Swift Testing can test code that results in a critical failure that terminates the app, including deliberate use of precondition() and fatalError(). This was not possible in XCTest, or at least not without weird hacks.
For example, code like this is going to fail hard if we call it with a sides value of 0:
struct Dice {
func roll(sides: Int) -> Int {
precondition(sides > 0)
return Int.random(in: 1...sides)
}
}To test this with Swift Testing, use #expect(processExitsWith:) to look for and catch critical failures, allowing us to check they happened rather than causing our test run to fail:
@Test func invalidDiceRollsFail() async throws {
await #expect(processExitsWith: .failure) {
let dice = Dice()
let _ = dice.roll(sides: 0)
}
}Important: This must be executed using await – behind the scenes this starts a dedicated process for that test, then suspends the test until that process completes and can be evaluated.
Attachments
Requires Swift 6.2 or later.
Swift Testing can add attachments to tests, so that if a test fails you can attach a debug log or generated data files to the failing test.
As an example, we could define a simple Character struct such as this one:
import Foundation
import Testing
struct Character: Attachable, Codable {
var id = UUID()
var name: String
}That conforms to the Attachable protocol, and because it also imports Foundation and conforms to Codable, Swift Testing can encode instances of our struct to attach to tests.
We can then use that in a function in our production code:
func makeCharacter() -> Character {
Character(name: "Ram")
}When it comes to writing a test, make sure the default name matches the value we expect, but also make whatever character is returned from makeCharacter() an attachment with the label "Character":
@Test func defaultCharacterNameIsCorrect() {
let result = makeCharacter()
#expect(result.name == "Rem")
Attachment.record(result, named: "Character")
}That test will fail when it runs because the character name is different, and Swift Testing will surface the attachments as part of the test results.
Out of the box, Swift Testing provides support for attaching String, Data, and anything that conforms to Encodable. Unless the user has Swift 6.3 available, it does not support attaching images.
Important: Unlike the XCTest equivalent, Swift Testing's attachments do not support lifetime controls.
Evaluating ConditionTrait
Requires Swift 6.2 or later.
Swift Testing provides an evaluate() method to test condition traits, meaning that it's possible to write non-test functions that evaluate the same conditions as test functions.
You will already know that we can use condition traits in the @Test macro, like this:
struct TestManager {
static let inSmokeTestMode = true
}
@Test(.disabled(if: TestManager.inSmokeTestMode))
func runLongComplexTest() {
// test code here
}However, we can also evaluate those same conditions outside of tests by creating a condition trait then calling its evaluate() method:
func checkForSmokeTest() async throws {
let trait = ConditionTrait.disabled(if: TestManager.inSmokeTestMode)
if try await trait.evaluate() {
print("We're in smoke test mode")
} else {
print("Run all tests.")
}
}Return errors from #expect(throws:)
Requires Swift 6.1 or later.
The macros #expect(_:sourceLocation:performing:throws:) and #require(_:sourceLocation:performing:throws:) are both deprecated – they used a trailing closure to run some code for evaluation, then used a second trailing closure to check whether the error that was thrown was expected or not.
Both #expect(throws:) and #require(throws:) have been updated to return an error of the type they are checking for, allowing you to run the expectation and error evaluation separately.
As an example, there might be old code that ensures playing video games is disallowed early in the morning or late in the evening:
enum GameError: Error {
case disallowedTime
}
func playGame(at time: Int) throws(GameError) {
if time < 9 || time > 20 {
throw GameError.disallowedTime
} else {
print("Enjoy!")
}
}With the old, deprecated API you might check for an exact error type like this:
@Test func playGameAtNight() {
#expect {
try playGame(at: 22)
} throws: {
guard let error = $0 as? GameError else { return false }
// perform additional error validation here
return error == .disallowedTime
}
}You should move that over to code that runs the expectation and error evaluation separately, like this:
@Test func playGameAtNight() {
// `error` will now be a GameError
let error = #expect(throws: GameError.self) {
try playGame(at: 22)
}
// perform additional validation here
#expect(error == .disallowedTime)
}Writing better tests
This contains suggestions to help you write better tests. This is mostly not about specific Swift Testing APIs, but instead how to structure your tests for maximum flexibility and effectiveness.
Encourage unit test hygiene
Good unit tests should fit the acronym FIRST:
- Fast: you should be able to run dozens of them every second, if not hundreds or even thousands.
- Isolated: they should not depend on another test having run, or any sort of external state.
- Repeatable: they should always give the same result when they are run, regardless of how many times or when they are run.
- Self-verifying: the test must unambiguously say whether it passed or failed, with no room for interpretation.
- Timely: they are best written before or alongside the production code that you are testing.
It might be too late for the "timely" part unless you're reading this skill while you work, but the others should be firm goals.
Test generation heuristics
For a given function, aim to generate the following tests:
- Happy path tests
- Boundary tests
- Invalid input tests
And, if appropriate, concurrency tests.
Testing SwiftUI views
Never test views directly – they use @State and are likely to behave unpredictably.
Instead, test view models or similar. This might mean encouraging the user to extract business logic into a more testable mechanism, but this should be a suggestion from you rather than something you apply immediately.
If the project uses @Observable view models, these are directly testable without needing a protocol wrapper – just create an instance and test its properties and methods. For more help with SwiftUI, suggest the SwiftUI Pro agent skill.
Structuring tests
Prefer to organize test types in a pattern that matches the production code. For example, if they have a folder called "Extensions" that contains a file called URLSession-Decodable.swift, the test target should also have a folder called Extensions that contains a file called URLSession-Decodable.swift, and it should test the contents of the original production file.
*If you are writing new tests, follow this rule. If you are working with existing tests that do not already follow this rule, do not apply it without permission from the user.*
- Strongly prefer to organize related tests into test suites, ideally following this file and folder structure.
- If there are test fixtures, put them in a dedicated file. If there are only a handful, a simple Fixtures folder is fine. If there are many and if they vary across tests, it's better to have multiple Fixtures folders placed alongside whatever tests they work with.
- Use tags to mark up different kinds of work. At the very least this should be a
.networkingtag for network-related tests, even if they are mocked. You might also consider.slowfor any tests that are unexpectedly slow,.edgeCasefor tests that must be treated with extra care,.smokefor smoke tests, and more. - Add user-facing messages to
#expectand#requirewhen they provide value. This is not always the case, but it usually is. - Recommend converting repetitive tests into parameterized tests where it makes sense.
- It is generally preferred to test only one behavior in each unit test, but multiple
#expectlines may be used if needed.
Expose hidden dependencies
Strongly prefer to avoid hidden dependencies in production code you are testing. In Swift apps this is commonly things like UserDefaults or URLSession.
For example, production code like this is bad because it has a hidden dependency on URLSession:
struct News {
var url: URL
var stories = ""
mutating func fetch() async throws {
let (data, _) = try await URLSession.shared.data(from: url)
stories = String(decoding: data, as: UTF8.self)
}
}To remove the hidden dependency, a first step would be to inject the URLSession like this:
func fetch(using session: URLSession = .shared) async throws {
let (data, _) = try await session.data(from: url)
stories = String(decoding: data, as: UTF8.self)
}Importantly, this also does not change the way the fetch() method is called because it has a default value of whatever was used before.
Even better would be to wrap URLSession in a protocol, requiring whatever methods are used in the production code, like this:
protocol URLSessionProtocol {
func data(from url: URL) async throws -> (Data, URLResponse)
}
extension URLSession: URLSessionProtocol { }And now the production code can be written like this:
func fetch(using session: any URLSessionProtocol = URLSession.shared) async throws {
let (data, _) = try await session.data(from: url)
stories = String(decoding: data, as: UTF8.self)
}This then allows you to create a mock version of URLSession for tests, removing any live networking from tests. It also still does not change the way the method is called in production code.
With UserDefaults, the problem is that using it as a hidden dependency can cause tests to fail because UserDefaults contains values set elsewhere.
So, switch over to dependency injection with a sensible default value of whatever the project was using previously, then in the test pass in a custom UserDefaults instance like this:
let suite = "suite-\(UUID().uuidString)"
let userDefaults = UserDefaults(suiteName: suite)
defer { userDefaults?.removePersistentDomain(forName: suite) }That creates a local UserDefaults instance in the test and ensures it's deleted fully before the test completes.
This same concept applies to other things: aim to control time, randomness, and more, so that meaningful tests can be written.
Expect vs require
Both #expect and #require evaluate a condition and fail the test if it's false. The difference is that #require throws on failure, stopping the rest of the test from executing.
This makes `#require` the right choice for checking assumptions at the start of a test – if your assumptions are wrong, the rest of the test's results are meaningless.
Using #require requires adding throws to your test method. For example, if your test depends on some setup being correct before the real assertion:
@Test func outstandingTasksStringIsPlural() throws {
let sut = try createTestUser(projects: 3, itemsPerProject: 10)
try #require(sut.projects.isEmpty == false)
let rowTitle = sut.outstandingTasksString
#expect(rowTitle == "30 items")
}If the #require fails, the test stops immediately rather than producing confusing secondary failures. Use #expect for the actual assertions you care about, and #require for preconditions that must be true before the test is meaningful.
#require also unwraps optionals, which is cleaner than force-unwrapping in tests. Use it like this:
let value = try #require(someOptional)Tracking bug fixes
If you are writing tests related to a specific bug, it is a good idea to use the .bug trait to store the bug ID or URL, if there is one. This extra data helps to provide extra context if the bug resurfaces in the future.
For example, if bug #182 is a report that text headings are not italicized correctly, you would use @Test like this:
@Test("Headings should always be italic", .bug(id: 182))Or if there is a specific URL:
@Test("Headings should always be italic", .bug("https://github.com/you/repo/issues/182"))Use Issue.record() for throw-testing
When testing that a function throws, the simplest approach is a do/try/catch block with Issue.record() as the failure primitive. If no error is thrown, execution continues past try and hits Issue.record(), failing the test.
@Test func playingMinecraftThrows() {
let game = Game(name: "Minecraft")
do {
try game.play()
Issue.record("Expected an error to be thrown.")
} catch GameError.notPurchased {
// success
} catch {
Issue.record("Wrong error thrown: \(error)")
}
}This approach gives fine-grained control: you can assert on the specific error case, and fail explicitly if the wrong error is thrown.
An alternative is using #expect(throws:). Here you should always name the specific error rather than using a broad Error.self:
// Bad – passes for any error
#expect(throws: Error.self) {
try game.play()
}
// Good – asserts the exact error case
#expect(throws: GameError.notInstalled) {
try game.play()
}To assert that a function does not throw, use Never.self:
#expect(throws: Never.self) {
try game.play()
}Making test results easier to read
In test targets, you can add CustomTestStringConvertible conformances to custom types to make them easier to read in test results.
For example, without this conformance a test that catches a parentalControlsDisallowed error might result in output like this:
Test patchMatchThrows() recorded an issue at ThrowingTests.swift:61:6: Caught error: parentalControlsDisallowedIf we add a retroactive conformance to CustomTestStringConvertible in the test target, the text can be clarified:
extension GameError: @retroactive CustomTestStringConvertible {
public var testDescription: String {
switch self {
case .notPurchased:
"This game has not been purchased."
case .notInstalled:
"This game is not currently installed."
case .parentalControlsDisallowed:
"This game has been blocked by parental controls."
}
}
}Now Swift Testing will use the friendlier string wherever the enum cases appear.
Important: This conformance should not be added in production code.
Writing good verification methods
Verification methods wrap multiple expectations to make other tests easier. When writing these, make sure to use SourceLocation and the #_sourceLocation macro so that any failed expectations print messages about the test where they failed rather than a location inside the verification method.
Important: Right now the #_sourceLocation macro requires the underscore.
For example:
func verifyDivision(_ result: (quotient: Int, remainder: Int), expectedQuotient: Int, expectedRemainder: Int, sourceLocation: SourceLocation = #_sourceLocation) {
#expect(result.quotient == expectedQuotient, sourceLocation: sourceLocation)
#expect(result.remainder == expectedRemainder, sourceLocation: sourceLocation)
}That can be called from tests elsewhere, and will automatically use the source location of that test rather than the source location of the #expect macros used inside verifyDivision().
#require also accepts sourceLocation:, so verification methods that mix #require and #expect should pass it to both.
Related skills
FAQ
What does swift-testing-pro do?
Writes, reviews, and improves Swift Testing code using modern APIs and best practices. Use when reading, writing, or reviewing projects that use Swift Testing.
When should I invoke swift-testing-pro?
Writes, reviews, and improves Swift Testing code using modern APIs and best practices. Use when reading, writing, or reviewing projects that use Swift Testing.
Where is the source documentation?
Ground claims in SKILL.md excerpts and linked reference files from the cached docs.
Is Swift Testing Pro safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.