How do you write unit tests in Swift? Compare Swift Testing and XCTest.
EasyAsked oftenTesting 2 min read
TL;DR
- Swift Testing (Xcode 16+):
@Testfunctions,#expectand#requiremacros, parameterized tests, runs in parallel by default. Use it for new unit tests. - XCTest:
XCTestCasesubclasses,test...methods,XCTAssert...functions. Still needed for UI tests and performance tests. - Both can live in the same test target, so you can migrate step by step.
A Swift Testing example
import Testing
@testable import Shop
@Suite("Cart")
struct CartTests {
@Test func emptyCartHasZeroTotal() {
let cart = Cart()
#expect(cart.total == 0)
}
@Test func addingItemsUpdatesTotal() throws {
var cart = Cart()
try cart.add(Item(price: 10), quantity: 2)
#expect(cart.total == 20)
#expect(cart.items.count == 1)
}
}#expectshows the actual values on failure, likecart.total → 15 == 20.- A new
CartTestsinstance is created for each test. Useinitfor setup anddeinit(in a class) for teardown.
#require: stop when a precondition fails
@Test func decodesFirstProduct() throws {
let products = try JSONDecoder().decode([Product].self, from: fixture)
let first = try #require(products.first) // unwraps or fails and stops
#expect(first.name == "Lamp")
}Parameterized tests
One test, many inputs. Each case shows as its own result.
@Test(arguments: [
("a@b.com", true),
("no-at-sign", false),
("", false)
])
func validatesEmail(input: String, isValid: Bool) {
#expect(EmailValidator.isValid(input) == isValid)
}Async and errors
@Test func loadsProfile() async throws {
let model = ProfileModel(api: FakeAPI(user: .sample))
try await model.load()
#expect(model.name == "Ana")
}
@Test func rejectsNegativeQuantity() {
var cart = Cart()
#expect(throws: CartError.invalidQuantity) {
try cart.add(Item(price: 5), quantity: -1)
}
}For callbacks, use confirmation:
@Test func notifiesOnSave() async {
await confirmation("saved") { saved in
let store = Store(onSave: { saved() })
await store.save()
}
}Traits
@Test(.disabled("Flaky on CI")) func syncs() async {}
@Test(.tags(.networking), .timeLimit(.minutes(1))) func downloads() async {}
@Suite(.serialized) struct DatabaseTests {} // run one by oneThe same tests in XCTest
import XCTest
@testable import Shop
final class CartXCTests: XCTestCase {
var cart: Cart!
override func setUp() {
super.setUp()
cart = Cart()
}
func testAddingItemsUpdatesTotal() throws {
try cart.add(Item(price: 10), quantity: 2)
XCTAssertEqual(cart.total, 20)
}
func testLoadsProfile() async throws {
let model = ProfileModel(api: FakeAPI(user: .sample))
try await model.load()
XCTAssertEqual(model.name, "Ana")
}
}| Swift Testing | XCTest | |
|---|---|---|
| Declare a test | @Test on any function | Method name starts with test |
| Assertions | #expect, #require | Many XCTAssert... functions |
| Setup | init / deinit | setUp / tearDown |
| Parameterized | Built in | Manual loops |
| Parallel | By default, in process | Optional, separate processes |
| UI and performance tests | No | Yes |
What makes a good unit test
- Fast: no real network, disk or sleeps.
- Isolated: no shared state between tests. Important now that tests run in parallel.
- Clear: Arrange, Act, Assert. One behavior per test. A name that says what is expected.
- Deterministic: inject clocks, random generators and UUIDs.
Common follow-up questions
- What does
@testable importdo? It lets tests seeinternalsymbols of the module. - How do you test
@MainActorcode? Mark the test or suite@MainActor. - How do you measure code coverage? Turn on "Gather coverage" in the scheme's test settings. Aim for important paths, not a number.
- What do you test in a view model? State changes after actions, error states, and calls to dependencies.
- Can Swift Testing run XCTest tests? They run side by side in one target. Do not mix their assertions in one test.