Swift Testing basics
A test is like a checklist a pilot runs before takeoff: set up the situation, do one thing, check that the result is what it should be. In Swift Testing, a test is just a function marked @Test. Inside, #expect(...) checks a condition. When a check fails, Xcode shows you the actual values on both sides, so you rarely need to add print statements to find out what went wrong.
Swift Testing replaces XCTestCase subclasses and the XCTAssert* family with @Test functions, #expect and #require macros that capture sub-expressions, value-type suites (a fresh instance per test, so init/deinit replace setUp/tearDown), parameterized tests with argument-level results, and traits (.tags, .enabled(if:), .disabled, .bug, .timeLimit, .serialized). Tests run in parallel by default, in-process, and it coexists with XCTest in the same target, which matters because UI tests and performance tests still use XCTest.
import Testing
@testable import Shop
struct PriceCalculatorTests {
let calculator = PriceCalculator(taxRate: 0.2) // a fresh instance for every test
@Test func addsTax() {
#expect(calculator.total(subtotal: 100) == 120)
}
@Test("A percentage coupon is applied before tax")
func percentageCoupon() throws {
let coupon = try #require(Coupon(code: "SAVE10")) // stops the test if nil
#expect(calculator.total(subtotal: 100, coupon: coupon) == 108)
}
@Test(arguments: [
(subtotal: 0, expected: 0),
(subtotal: 10, expected: 12),
(subtotal: 99.99, expected: 119.99),
])
func totals(subtotal: Decimal, expected: Decimal) {
#expect(calculator.total(subtotal: subtotal) == expected)
}
@Test func rejectsNegativeSubtotal() {
#expect(throws: PriceError.negativeAmount) {
try calculator.validatedTotal(subtotal: -5)
}
}
}@Testmarks a test; the string is an optional readable name for the test navigator.#expect(a == b)checks and continues; on failure it shows both values, like120.5 → 120.try #require(x)unwraps an optional or checks a condition and stops the test if it fails, so the rest doesn’t crash on anil.arguments:runs the same test once per input, and each input shows up as its own result.#expect(throws:)checks that code throws a specific error.- The struct gets a new instance for each test, so tests never share state through properties.
Predict the output
The totals test above has three arguments. The second one is wrong in the code under test. How many results does the test report?
Each argument runs as a separate test case: three results, two passing and one failing, and you can re-run just the failing argument from the test navigator. Compare a for loop inside one test, which reports a single failure and makes you work out which input it was.
Test a password rule
PasswordRule.check(_:) returns a list of problems: .tooShort (under 8 characters), .noDigit, .noLetter. Write a parameterized test that checks four passwords and the exact problems each one should produce.
›
Pass pairs of (password, expected problems) as arguments. A Set of problems makes the comparison independent of order.
SolutionTry it yourself first
struct PasswordRuleTests {
@Test(arguments: [
("abc", Set<PasswordProblem>([.tooShort, .noDigit])),
("12345678", [.noLetter]),
("abcdefgh", [.noDigit]),
("abcd1234", []),
])
func problems(password: String, expected: Set<PasswordProblem>) {
#expect(Set(PasswordRule.check(password)) == expected)
}
}When a case fails, the result shows which password it was and both sets, for example ["tooShort"] → ["tooShort", "noDigit"].
Traits and organization
- Tags (
@Tag static var checkout: Self) group tests across files for test plans and filtering. - Conditions:
.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil),.disabled("waiting for API v3"),.bug("https://…", "FB12345")for known failures (paired withwithKnownIssue { }to keep the suite green while recording the issue). .timeLimit(.minutes(1))fails hung async tests instead of blocking CI.- Parallelism: tests and parameterized cases run concurrently. Global and static state, shared singletons, the file system and
UserDefaults.standardcause order-dependent flakiness; inject them, or mark the suite.serializedas a last resort. - Migration: keep XCTest for UI tests and
measure {}performance tests; convert unit tests file by file, since both frameworks run in the same target.
@Testfunctions,#expect(continues, captures sub-expressions),try #require(stops, unwraps).- Suites are types with per-test instances (
init/deinitas setup/teardown); parameterized tests report per argument. - Traits: tags,
.enabled(if:),.disabled,.bug+withKnownIssue,.timeLimit,.serialized. - Parallel by default: no shared static state. XCTest stays for UI and performance tests; both coexist.
Check this part
What's the difference between #expect and #require?
Why use @Test(arguments:) instead of a for loop in one test?
Tests pass alone but fail randomly together. What's the likely cause in Swift Testing?