Xcode13からXCTest
周りの機能が強化され、特にflaky testに対応しやすくなったので、実際の対応方法をいくつか書きます。
Flaky testの再現確認
まず始めに、失敗する状況を手元で再現できれば原因を特定できる可能性があるので、Xcodeの機能を使って繰り返し実行してみることをおすすめします。
Xcode13から追加された機能で、Xcodeから操作する場合は、テストケースの左側にあるボタンから実行することができます。
本題から少し外れてしまうので、詳しいオプションはドキュメントを参照してください。
developer.apple.com
xcodebuild
で行う場合は、-test-iterations
というオプションで実行可能です。
XCTSkipを利用してskipする
特定の環境のみで再現するケースでは、暫定対応としてまずはXCTSkip
を使って該当する環境でskipすることをおすすめします。
単純にコメントアウトする場合と異なり、skipする条件を指定できるので、正常に動いている環境ではテストを実行したままにすることが出来ます。
func testSkip() throws {
try XCTSkipIf(environment.isCI, "<https://hoge1234fuga.com/bugs/5678> hoge bug")
}
またXCTSkip
を利用した場合、skipされたテストケースも出力には残るため、集計の点でもメリットがあります。
XCTExpectedFailureを利用して結果を成功扱いにする
Flaky testになっている原因が特定できない場合は、XCTExpectedFailure
を使うことで、CIなどでテストケースを実行させながら、結果は成功扱いにすることが出来ます。
XCTSkip
を使う場合よりも、テストケースの実行を維持できる点と、場合によっては失敗した原因も探ることができる点で、いくらかメリットがあります。
func testExpectedFailure() throws {
XCTExpectFailure("<https://hoge1234fuga.com/bugs/5678> hoge bug", strict: false) {
}
}
func testExpectedFailure2() throws {
XCTExpectFailure("<https://hoge1234fuga.com/bugs/5678> hoge bug", strict: false)
}
strict
オプションはデフォルトでtrue
ですが、この場合は逆にブロック内のテストが成功した場合に、テストケースが失敗扱いになってしまうため、flaky testの場合はfalse
にする必要があります。
さらに細かく、テストケースの失敗の原因のうち一部のみを許容したい場合は、issueMatcher
と組み合わせることが出来ます。
例としてHogeError.expected
だけ許容するケースを考えてみます。
func testMatcherExpectedFailure() throws {
try XCTExpectFailure("<https://hoge1234fuga.com/bugs/5678> hoge bug", strict: false) {
throw HogeError.expected
} issueMatcher: { issue in
issue.type == .thrownError && issue.associatedError as? HogeError == .expected
}
}
func testMatcherExpectedFailure2() throws {
try XCTExpectFailure("<https://hoge1234fuga.com/bugs/5678> hoge bug", strict: false) {
throw HogeError.unexpected
} issueMatcher: { issue in
issue.type == .thrownError && issue.associatedError as? HogeError == .expected
}
}
func testMatcherExpectedFailure3() {
XCTExpectFailure("<https://hoge1234fuga.com/bugs/5678> hoge bug", strict: false) {
print("hoge")
} issueMatcher: { issue in
issue.type == .thrownError && issue.associatedError as? HogeError == .expected
}
}
失敗の原因のうちissueMatcher
でマッチしなかったものは、XCTExpectFailure
を書かない場合と同じ扱いで、失敗になります。
実際の出力は以下のようになります。
Test Case '-[__lldb_expr_31.HogeTests testMatcherExpectedFailure]' started.
XCTExpectFailure: matcher accepted Thrown Error: failed: caught error: "expected"
Test Case '-[__lldb_expr_31.HogeTests testMatcherExpectedFailure]' passed (0.034 seconds).
Test Case '-[__lldb_expr_31.HogeTests testMatcherExpectedFailure2]' started.
XCTExpectFailure: matcher rejected Thrown Error: failed: caught error: "unexpected""unexpected"
Test Case '-[__lldb_expr_31.HogeTests testMatcherExpectedFailure2]' failed (0.031 seconds).
Test Case '-[__lldb_expr_31.HogeTests testMatcherExpectedFailure3]' started.
hoge
Test Case '-[__lldb_expr_31.HogeTests testMatcherExpectedFailure3]' passed (0.002 seconds).
issueMatcher
の引数のXCTIssue
については、type
やassociatedError
が情報として活用できますが、不足する場合はdetailedDescription
などの文字列を見るケースもあり得ます。
developer.apple.com
動画リンク
今回は結構ざっくりしていて、ドキュメントもわかりづらいので、実際に動かすのが一番良いかもです
developer.apple.com
developer.apple.com