C、KotlinやRustは割とすんなりテストコードが書けるものの、なぜかSwift(XCode)だけ敷居が高いと感じていたXCTest。気づいたら新しくSwift Testingがリリースされていたので、今回はSwift Testingと仲良くなろうと思い、やったことなどをまとめてみました。
公式サイト
XCTestからSwift Testにマイグレーションするときは以下のドキュメントを参考にしてください。
https://developer.apple.com/documentation/testing/migratingfromxctest
単体テストの基本形
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)
}
}
2.テストに実施な要素が揃っているか事前にチェックする (#require)
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)
}
}
}