Swift testing
Modern Swift Testing framework patterns and best practicesFrom its SKILL.md
npx -y skills add onmyway133/skills --skill swift-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
8.9 KB, as published. Nobody here has run it
Swift Testing
You are a Swift testing expert using the modern Swift Testing framework (not XCTest). Apply these patterns when writing tests.
Framework Basics
Import and Structure
import Testing
@testable import MyApp
struct UserTests {
@Test func createsUserWithValidEmail() {
let user = User(email: "[email protected]")
#expect(user.isValid)
}
}
Key Differences from XCTest
| XCTest | Swift Testing |
|---|---|
class with XCTestCase | struct (preferred) or class |
func testSomething() | @Test func something() |
XCTAssertEqual(a, b) | #expect(a == b) |
XCTAssertTrue(x) | #expect(x) |
XCTAssertNil(x) | #expect(x == nil) |
XCTAssertThrowsError | #expect(throws:) |
setUpWithError() | init() |
tearDown() | deinit |
Expectations
Basic Expectations
@Test func basicExpectations() {
let value = 42
// Boolean
#expect(value > 0)
#expect(value == 42)
// Optionals
let optional: String? = "hello"
#expect(optional != nil)
// Collections
let array = [1, 2, 3]
#expect(array.contains(2))
#expect(array.count == 3)
}
Require (Unwrapping)
@Test func requireUnwrapping() throws {
let optional: String? = "hello"
// Unwrap or fail test
let value = try #require(optional)
#expect(value == "hello")
// With custom message
let user = try #require(fetchUser(), "User should exist")
#expect(user.isActive)
}
Error Expectations
@Test func errorHandling() {
// Expect any error
#expect(throws: (any Error).self) {
try riskyOperation()
}
// Expect specific error type
#expect(throws: ValidationError.self) {
try validate(email: "invalid")
}
// Expect specific error value
#expect(throws: ValidationError.invalidEmail) {
try validate(email: "")
}
}
Test Organization
Suites (Grouping)
@Suite("User Authentication")
struct AuthenticationTests {
@Suite("Login")
struct LoginTests {
@Test func validCredentials() { }
@Test func invalidPassword() { }
}
@Suite("Registration")
struct RegistrationTests {
@Test func newUser() { }
@Test func duplicateEmail() { }
}
}
Setup and Teardown
struct DatabaseTests {
let database: Database
// Setup in init
init() async throws {
database = try await Database.createTest()
}
@Test func insertsRecord() async throws {
try await database.insert(record)
#expect(await database.count == 1)
}
}
Parameterized Tests
Basic Parameterization
@Test(arguments: ["hello", "world", "swift"])
func stringIsNotEmpty(value: String) {
#expect(!value.isEmpty)
}
@Test(arguments: 1...10)
func positiveNumbers(n: Int) {
#expect(n > 0)
}
Multiple Parameters
@Test(arguments: [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0)
])
func addition(a: Int, b: Int, expected: Int) {
#expect(a + b == expected)
}
Zip vs Product
// Zip: pairs elements (3 tests)
@Test(arguments: zip([1, 2, 3], ["a", "b", "c"]))
func zipped(number: Int, letter: String) { }
// Product: all combinations (9 tests)
@Test(arguments: [1, 2, 3], ["a", "b", "c"])
func product(number: Int, letter: String) { }
Tags and Traits
Tagging Tests
extension Tag {
@Tag static var slow: Self
@Tag static var network: Self
@Tag static var critical: Self
}
@Test(.tags(.slow, .network))
func networkRequest() async { }
@Test(.tags(.critical))
func paymentProcessing() { }
Display Names
@Test("User can update profile picture")
func updateProfilePicture() { }
@Suite("Payment Processing")
struct PaymentTests { }
Conditional Tests
// Disable with reason
@Test(.disabled("Waiting for API update"))
func upcomingFeature() { }
// Platform-specific
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil))
func ciOnly() { }
// Runtime conditions
@Test
func featureFlagTest() throws {
try #require(FeatureFlags.newCheckout, "Feature not enabled")
// Test new checkout...
}
Time Limits
@Test(.timeLimit(.minutes(1)))
func longRunningOperation() async { }
@Test(.timeLimit(.seconds(5)))
func quickOperation() { }
Async Testing
Async Tests
@Test func asyncFetch() async throws {
let service = DataService()
let result = try await service.fetchData()
#expect(!result.isEmpty)
}
Actor Testing
@Test func actorState() async {
let counter = Counter()
await counter.increment()
await counter.increment()
let value = await counter.value
#expect(value == 2)
}
Confirmation (Async Expectations)
@Test func notificationReceived() async {
await confirmation { confirm in
NotificationCenter.default.addObserver(
forName: .userLoggedIn,
object: nil,
queue: nil
) { _ in
confirm()
}
await loginUser()
}
}
// Multiple confirmations
@Test func multipleEvents() async {
await confirmation(expectedCount: 3) { confirm in
eventHandler.onEvent = { confirm() }
triggerThreeEvents()
}
}
Best Practices
Naming
// Good: describes behavior
@Test func calculatesDiscountForPremiumUsers() { }
@Test func throwsErrorWhenEmailInvalid() { }
@Test func returnsEmptyArrayWhenNoResults() { }
// Avoid: vague or implementation-focused
@Test func testDiscount() { } // What about discount?
@Test func testMethod() { } // Which method?
Arrange-Act-Assert
@Test func appliesCouponCode() throws {
// Arrange
let cart = ShoppingCart()
cart.addItem(Product(price: 100))
let coupon = Coupon(code: "SAVE20", discount: 0.20)
// Act
try cart.apply(coupon)
// Assert
#expect(cart.total == 80)
#expect(cart.appliedCoupons.contains(coupon))
}
One Concept Per Test
// Good: focused tests
@Test func addsItemToCart() {
cart.add(item)
#expect(cart.items.contains(item))
}
@Test func updatesCartTotal() {
cart.add(item)
#expect(cart.total == item.price)
}
// Avoid: testing multiple concepts
@Test func cartOperations() {
cart.add(item)
#expect(cart.items.contains(item))
cart.remove(item)
#expect(cart.items.isEmpty)
cart.add(item)
cart.add(item2)
#expect(cart.total == item.price + item2.price)
}
Test Fixtures
struct OrderTests {
// Shared fixtures
let validProduct = Product(id: "1", name: "Widget", price: 9.99)
let customer = Customer(id: "c1", tier: .standard)
@Test func createsOrder() {
let order = Order(customer: customer, items: [validProduct])
#expect(order.isValid)
}
@Test func calculatesShipping() {
let order = Order(customer: customer, items: [validProduct])
#expect(order.shippingCost == 5.99)
}
}
Migration from XCTest
Swift Testing was introduced at WWDC 2024 and ships with Xcode 16. XCTest is not deprecated - migrate incrementally at your own pace.
What Can Be Migrated
- Unit tests with assertions
- Async/await tests
- Tests using setUp/tearDown
What Cannot Be Migrated
- UI tests using XCUIApplication
- Performance tests using XCTMetric
- Objective-C tests
Continue using XCTest for these cases.
Critical Rule
Never mix frameworks within a single test. Don't call XCTAssert from Swift Testing or #expect from XCTest.
Complete Migration Table
| XCTest | Swift Testing |
|---|---|
class MyTests: XCTestCase | struct MyTests |
func testSomething() | @Test func something() |
override func setUp() | init() |
override func tearDown() | deinit |
XCTAssert(x) | #expect(x) |
XCTAssertTrue(x) | #expect(x) |
XCTAssertFalse(x) | #expect(!x) |
XCTAssertNil(x) | #expect(x == nil) |
XCTAssertNotNil(x) | #expect(x != nil) |
XCTAssertEqual(a, b) | #expect(a == b) |
XCTAssertNotEqual(a, b) | #expect(a != b) |
XCTAssertGreaterThan(a, b) | #expect(a > b) |
XCTAssertLessThan(a, b) | #expect(a < b) |
XCTUnwrap(x) | try #require(x) |
XCTFail("message") | Issue.record("message") |
XCTAssertThrowsError | #expect(throws:) |
XCTAssertNoThrow | #expect(throws: Never.self) |
expectation + fulfill + wait | await confirmation |
Running Tests
Command Line
# Run all tests
swift test
# Run specific suite
swift test --filter UserTests
# Run with tags
swift test --filter "tag:critical"
# Parallel execution (default)
swift test --parallel
Xcode
Cmd+U- Run all testsCmd+Ctrl+U- Run test at cursor- Test Navigator shows results organized by suite