Swift Testingと仲良くなる
C++、KotlinやRustは割とすんなりテストコードが書けるものの、なぜかSwift(XCode)だけ敷居が高いと感じていたXCTest
。気づいたら新しくSwift Testing
がリリースされていたので、今回はSwift Testing
と仲良くなろうと思い、やったことなどをまとめてみました。
公式サイト
XCTest
からSwift Test
にマイグレーションするときは以下のドキュメントを参考にしてください。
単体テストの基本形
XCTest
からSwift Test
に変わり、評価関数が#expect
と#require
のみとかなりシンプルになりました。テストのショートカットキーが⌘U
なので ⌘Command + u
でテストが実行できます。
テストコード
import Testing
@Suite("Swift Test Demo") struct SwiftTestDemo {
@Test("Simple test") func example() {
#expect(1 == 1)
}
}
成功パターン
Test run started.
Testing Library Version: 102 (x86_64-apple-macos13.0)
Suite "Swift Test Demo" started.
Test "Simple test" started.
Test "Simple test" passed after 0.001 seconds.
Suite "Swift Test Demo" passed after 0.001 seconds.
Test run with 1 test passed after 0.001 seconds.
失敗パターン
XCodeの画面でビルドエラーと同じように#expect
で失敗した箇所が赤く表示されます。
テストコードを詳しくみてみる
基本形のコードを1行ずつ詳しくみてみましょう。
Swift Testing
のロード
XCTest
からTesting
に変わっただけです。
import Testing
@Suite
マクロ
テストスイートとしてまとめるマクロです。
@Suite("Swift Test Demo") struct SwiftTestDemo {
// ...
}
@Suite
マクロは、引数を2つ以上設定することができます。@Test
がstruct
,actor
,class
内に含まれる場合は、明示する必要はありません。引数にdisplayName
を設定したり、SuiteTrait
を指定したいときはマクロを明記する必要があります。
定義
@attached(member) @attached(peer)
macro Suite(
_ displayName: String? = nil,
_ traits: any SuiteTrait...
)
SuiteTrait
について
端的に言うと、テストに対して条件を加えたり補足情報を加えたりできるTraitになります。種類は以下の通りです。
Topics | 利用方法 |
---|---|
.enabled |
テストの有効化 |
.disabled |
テストの有効化 |
.timeLimit |
実行時間の制限を加える |
.serializes |
テストの実行を連続・並列化する |
.tags |
テストに対してタグ情報を加える |
.bug |
テストにバグ情報(Issueなど)を追加する |
.comment |
テストにコメントを加える |
.prepare |
事前にテスト合格が必要なテストを関連付ける |
@Test
マクロ
@Test
はテスト対象であることを明示するマクロです。テスト実施に必須のマクロです。
@Test("Simple test") func example() {
// ...
}
定義
@attached(peer)
macro Test(
_ displayName: String? = nil,
_ traits: any TestTrait...
)
TestTrait
について
Trait
プロトコルを継承しているため、SuiteTrait
と同様にTrait
が利用可能です。テストに対して条件を加えたり補足情報を加えたりできるTraitになります。種類は以下の通りです。
Topics | 利用方法 |
---|---|
.enabled |
テストの有効化 |
.disabled |
テストの有効化 |
.timeLimit |
実行時間の制限を加える |
.serializes |
テストの実行を連続・並列化する |
.tags |
テストに対してタグ情報を加える |
.bug |
テストにバグ情報(Issueなど)を追加する |
.comment |
テストにコメントを加える |
.prepare |
事前にテスト合格が必要なテストを関連付ける |
リファレンス
https://developer.apple.com/documentation/testing/test(::arguments:)-3rzok
#expect
マクロ
かなりシンプルです。
#expect(1 == 1)
XCTestとSwift Testの評価関数対応表
色々とテストパターンを考えてみる
1.オブジェクト共通化
クラスのテストコードを書く場合、毎回生成と破棄を繰り返さなくてもよい場合はsetup
とteardown
を活用して、オブジェクト共通化をすることでテスト実施の高速化が期待できます。
class Calculator {
func add(_ x: Int, _ y: Int) -> Int {
return x + y
}
}
@Suite struct CalculatorTest {
// オブジェクト共通化
fileprivate let calc: Calculator;
// setup関数相当
init() {
self.calc = .init()
}
@Test func addTest() {
let expected: Int = 7;
let actual = calc.add(2, 5)
#expect(actual == expected)
}
@Test func addMinusValueTest() {
let expected: Int = -2;
let actual = calc.add(-5, 3)
#expect(actual == expected)
}
}
#require
)
2.テストに実施な要素が揃っているか事前にチェックする (try #require()
を使って、検査実施前に要素が揃っているかチェックします。
@Suite struct CalculatorTest {
// オブジェクト共通化
fileprivate let calc: Calculator;
// setup関数相当
init() {
self.calc = .init()
}
> @Test func addTest() throws {
let expected: Int = 7;
let actual = calc.add(2, 5)
+ try #require(calc != nil)
#expect(actual == expected)
}
+ // テスト失敗のケース
+ @Test func addTestExpectsTestFailed() throws {
+ let calc : Calculator? = nil
+ let expected: Int = 7;
+
+ try #require(calc != nil)
+
+ let actual = calc!.add(2, 5)
+ #expect(actual == expected)
+ }
}
3.期待する例外が発生したかチェックする
エラー系のテストケースです。#expect
を使う点は変わりません。
class Calculator {
func add(_ x: Int, _ y: Int) -> Int {
return x + y
}
+ func divide(_ numerator: Int, _ denominator: Int) throws -> Double {
+ if denominator == 0 {
+ throw CalculatorError.NotDividedByZero;
+ }
+
+ return Double((numerator/denominator));
+ }
}
import Foundation
enum CalculatorError: LocalizedError {
case Unknown
case NotDividedByZero
var errorDescription: String? {
switch self {
case .Unknown: return "Unknown error happened"
case .NotDividedByZero: return "Not devided by Zero"
}
}
}
@Suite struct CalculatorTest {
// オブジェクト共通化
fileprivate let calc: Calculator;
// setup関数相当
init() {
self.calc = .init()
}
// 何かしらのErrorが発生したか検査する
@Test func throwAnyCalculatorErrorTest() throws {
#expect(throws: CalculatorError.self) {
try calc.divide(2, 0)
}
}
// NotDividedByZeroエラーがきちんとが投げられたか検査する
@Test func throwDividedByZeroErrorTest() throws {
#expect(throws: CalculatorError.NotDividedByZero) {
try calc.divide(2, 0)
}
}
// 期待するErrorと異なるエラーがが投げられたか検査する
@Test func throwMissingErrorTest() throws {
#expect(throws: CalculatorError.Unknown) {
try calc.divide(2, 0)
}
}
}
Discussion