Chuyển một bộ test từ XCTest sang Swift Testing
Swift Testing là một cải tiến thật so với XCTest chứ không phải một cú đổi tên. Việc chuyển đổi diễn ra dần dần — hai framework chạy chung trong một target — và những phần tốt lên thì xứng đáng với một buổi chiều.
Bảng ánh xạ
// XCTest
final class ValidatorTests: XCTestCase {
func testValidEmailPasses() {
XCTAssertTrue(Validator.isValidEmail("a@b.com"))
}
func testInvalidEmailFails() throws {
let result = try Validator.check("nope")
XCTAssertEqual(result, .invalid)
}
}
// Swift Testing
struct ValidatorTests {
@Test func validEmailPasses() {
#expect(Validator.isValidEmail("a@b.com"))
}
@Test func invalidEmailFails() throws {
let result = try Validator.check("nope")
#expect(result == .invalid)
}
}
| XCTest | Swift Testing |
|---|---|
XCTAssertTrue(x) |
#expect(x) |
XCTAssertEqual(a, b) |
#expect(a == b) |
XCTAssertNil(x) |
#expect(x == nil) |
XCTUnwrap(x) |
try #require(x) |
XCTFail("…") |
Issue.record("…") |
XCTAssertThrowsError |
#expect(throws: MyError.self) { … } |
setUp() |
init() |
tearDown() |
deinit |
XCTestExpectation |
await confirmation { … } |
Hai điều suy ra từ việc #expect nhận vào một biểu thức thông thường: chỉ có một macro khẳng định
thay vì bốn mươi cái, và thông báo lỗi hiện ra giá trị — #expect(count == 3) hỏng với dòng
“Expectation failed: (count → 5) == 3”, thứ mà XCTest không làm được.
#expect so với #require
#expect ghi nhận một lỗi rồi chạy tiếp. #require ném lỗi và dừng bài test.
@Test func loadsProfile() async throws {
let profile = try #require(await api.profile(for: 42)) // dừng nếu nil
#expect(profile.name == "Tuan") // ghi nhận rồi chạy tiếp
#expect(profile.email.contains("@"))
}
Cách này tốt hơn mô hình của XCTest, nơi một lần mở nil hoặc làm sập cả lượt chạy hoặc đòi
XCTUnwrap kèm một guard. Hãy dùng #require cho các điều kiện tiên quyết và #expect cho những
khẳng định mà bạn muốn có đủ cả, kể cả khi một cái hỏng.
Test tham số hóa mới là cái lợi thật
Đây là thứ đã dẹp đi một phần ba lượng code test của tôi:
@Test(arguments: [
("a@b.com", true),
("no-at-sign", false),
("", false),
("@nolocal.com", false),
("spaces in@email.com", false),
])
func emailValidation(input: String, expected: Bool) {
#expect(Validator.isValidEmail(input) == expected)
}
Năm bài test riêng biệt, mỗi cái được báo cáo riêng, từ một hàm duy nhất. Trong XCTest thì đây hoặc là
năm phương thức gần như y hệt nhau, hoặc là một vòng for bên trong một bài test — và bản dùng vòng
lặp báo một lỗi cho năm trường hợp rồi dừng ở cái đầu tiên.
Hai tham số cho ra tích chéo:
@Test(arguments: [1, 2, 3], ["a", "b"])
func combinations(number: Int, letter: String) { … } // sáu bài test
Mẹo
Phần báo cáo mới là điểm mấu chốt. Mỗi trường hợp là một bài test riêng trong trình điều hướng với các tham số nằm trong tên, nên một lỗi cho bạn biết đầu vào nào đã vỡ mà không phải đọc cái vòng lặp. Xcode cũng cho bạn chạy lại riêng một trường hợp bị hỏng.
Trait
Siêu dữ liệu gắn vào các bài test, thay thế cho vài quy ước của XCTest:
@Test(.disabled("chập chờn trên CI"))
@Test(.bug("https://github.com/example/issues/42"))
@Test(.timeLimit(.minutes(1)))
@Test(.tags(.networking))
@Suite(.serialized) // chạy các test của suite này theo thứ tự
.disabled kèm lý do thì tốt hơn hẳn việc comment một bài test lại hay thêm tiền tố x vào tên nó
— bài test vẫn biên dịch nên nó không mục ruỗng, và lý do thì nhìn thấy được trong báo cáo.
.serialized quan trọng vì Swift Testing chạy các test song song theo mặc định, kể cả bên trong
một suite. XCTest chạy tuần tự trong phạm vi một class.
Khác biệt đã bẫy tôi
Chính tính song song đó, kết hợp với thay đổi còn lại: một thực thể mới của kiểu suite được tạo ra cho mỗi bài test.
struct DatabaseTests {
let database: Database
init() async throws {
database = try await Database(inMemory: true) // chạy trước MỌI bài test
}
}
Điều đó là tốt — nó dẹp trạng thái dùng chung giữa các bài test, nơi sinh ra sự chập chờn. Nhưng bộ
test XCTest của tôi đã tích tụ những bài test lặng lẽ phụ thuộc vào việc chạy đúng thứ tự và vào một
thực thể XCTestCase dùng chung, và nửa tá trong số đó hỏng ngay lập tức khi chạy song song.
Mọi cái hóa ra đều là vấn đề thật: những bài test chỉ pass vì một bài test trước đó đã để lại trạng thái. Sửa chúng mới là giá trị thật của cuộc chuyển đổi, và đó không phải thứ tôi chuyển đổi để có.
Với phần thiết lập tốn kém thật sự cần dùng chung, một static let hay một actor là chạy được, còn
.serialized là cửa thoát hiểm khi một suite thật sự không chạy song song được.
Chuyển đổi dần dần
Hai framework sống chung trong một target, nên không có cú chuyển đổi một phát ăn ngay nào. Thứ đã hiệu quả:
- Bài test mới thì viết bằng Swift Testing ngay từ ngày đầu.
- Chuyển một file khi động vào nó vì một lý do khác.
- Chuyển các ứng viên tham số hóa trước — bất cứ thứ gì hiện là năm phương thức gần như y hệt hay một vòng lặp. Đó là chỗ có lợi ích ngay lập tức.
- Để yên các bài test giao diện.
XCUIApplicationvẫn thuộc XCTest, và chưa có thứ tương đương bên Swift Testing.
Một lưu ý: các bài test hiệu năng. Chưa có thứ tương đương với measure { }, nên mọi thứ dùng
XCTMetric tạm thời ở lại XCTest.