8.3K Views
September 25, 24
スライド概要
関西モバイルアプリ研究会A #5 - connpass
https://kanmoba.connpass.com/event/329447/
#関モバ でSwift Testingの話をしました - usami-kの日記
https://usami-k.hatenablog.com/entry/2024/09/26/000522
https://usami-k.github.io/
XCTest から Swift Testing へ 宇佐見公輔 2024-09-25 株式会社ゆめみ
自己紹介 • 宇佐見公輔 • 株式会社ゆめみ ‣ iOS テックリード • iOSDC Japan 2024 ‣ パンフレット記事 / ポスターセッション 1 / 24
Swift Testing とは
Swift Testing とは • Swift 用の単体テストフレームワーク ‣ 従来の XCTest に比べて、Swift の機能をより活用 • Swift 公式の GitHub リポジトリで公開されている ‣ https://github.com/swiftlang/swift-testing • Xcode 16 に統合された ‣ Xcode の UI と連携する Swift Testing とは 3 / 24
XCTest と Swift Testing • XCTest ‣ 従来からの単体テストフレームワーク • Objective-C 時代から存在、Swift にも適合 ‣ https://github.com/swiftlang/swift-corelibsxctest • XCTest と Swift Testing は同じプロジェクトで混在可能 ‣ そのため、少しずつ移行していくことが可能 Swift Testing とは 4 / 24
Swift Testing の基本
テスト関数定義:XCTest import XCTest class FoodTruckTests: XCTestCase { func testEngineWorks() { // ... } } • XCTestCase のサブクラス内で定義する必要 • メソッド名を test 始まりで命名する必要 Swift Testing の基本 6 / 24
テスト関数定義:Swift Testing import Testing struct FoodTruckTests { @Test func engineWorks() { // ... } } • テスト関数はどこで定義してもよい • @Test 属性をつければメソッド名は自由 Swift Testing の基本 7 / 24
テストの実装 // XCTest func testEngineWorks() throws { XCTAssertNotNil(engine.parts.first) XCTAssertGreaterThan(engine.batteryLevel, 0) XCTAssertTrue(engine.isRunning) } // Swift Testing @Test func engineWorks() throws { try #require(engine.parts.first != nil) #expect(engine.batteryLevel > 0) #expect(engine.isRunning) } Swift Testing の基本 8 / 24
Optional 値の Unwrap // XCTest func testEngineWorks() throws { let part = try XCTUnwrap(engine.parts.first) } // Swift Testing @Test func engineWorks() throws { let part = try #require(engine.parts.first) } Swift Testing の基本 9 / 24
テスト実行時の setup/teardown // XCTest class FoodTruckTests: XCTestCase { override func setUp() async throws { // ... } } // Swift Testing final class FoodTruckTests { init() async throws { // ... } } Swift Testing の基本 10 / 24
変換ツール • XCTest のコードを Swift Testing に変換するツール ‣ https://github.com/giginet/swift-testingrevolutionary Swift Testing の基本 11 / 24
Swift Testing の機能
パラメータを変えてテスト enum Food { case burger, iceCream, burrito, noodleBowl, kebab } @Test(arguments: [ Food.burger, .iceCream, .burrito, .noodleBowl, .kebab ]) func foodAvailable(_ food: Food) async throws { let foodTruck = FoodTruck(selling: food) #expect(await foodTruck.cook(food)) } Swift Testing の機能 13 / 24
複数のパラメータ @Test(arguments: zip(Food.allCases, 1 ... 100)) func makeLargeOrder(of food: Food, count: Int) async throws { let foodTruck = FoodTruck(selling: food) #expect(await foodTruck.cook(food, quantity: count)) } Swift Testing の機能 14 / 24
テスト実行制御 @Test(.enabled(if: FoodTruck.sells(.arepas))) func arepasAreTasty() { // ... } @Test(.timeLimit(.seconds(30)) func serveLargeOrder() { // ... } Swift Testing の機能 15 / 24
アノテーション @Test func grillWorks() async { withKnownIssue("Grill is out of fuel") { try FoodTruck.shared.grill.start() } } Swift Testing の機能 16 / 24
非同期処理のテスト
非同期処理のテスト:XCTest func testTruckEvents() async { let soldFood = expectation(description: "…") FoodTruck.shared.eventHandler = { event in soldFood.fulfill() } await Customer().buy(.soup) await fulfillment(of: [soldFood]) } expectation 非同期処理のテスト を使う。fulfill() が呼ばれたら成功。 18 / 24
非同期処理のテスト:Swift Testing @Test func truckEvents() async { await confirmation("…") { soldFood in FoodTruck.shared.eventHandler = { event in soldFood() } await Customer().buy(.soup) } } confirmation 非同期処理のテスト を使う。confirmed が呼ばれたら成功。 19 / 24
注意点 • XCTest の expectation では await fulfillment() で完了 待ちをしていた。 • Swift Testing の confirmation は完了待ちをしない。ブ ロックを抜けるまでに confirmed メソッドが呼ばれない と失敗扱い。 この点では、不便になっているようにも見えるが・・・ 非同期処理のテスト 20 / 24
解決策(1) Swift Concurrency の withCheckedContinuation を使う。 @Test func truckEvents() async { await confirmation("…") { soldFood in await withCheckedContinuation { continuation in FoodTruck.shared.eventHandler = { event in soldFood() continuation.resume() } await Customer().buy(.soup) } } } 非同期処理のテスト 21 / 24
解決策(2) pointfreeco/swift-concurrency-extras の megaYield を使う。 @Test func truckEvents() async { await confirmation("…") { soldFood in FoodTruck.shared.eventHandler = { event in soldFood() } await Customer().buy(.soup) await Task.megaYield() } } 非同期処理のテスト 22 / 24
まとめ
XCTest から Swift Testing へ • XCTest から段階的に移行できる • 機械的に移行できる部分も多い • パラメータテストが便利 • 非同期処理のテストは少し変わっているので注意 まとめ 24 / 24