XCTSkipの使い方まとめ

#はじめに
Xcode11.4からXCTestフレームワークに新しく導入されたプロパティの一つに「[XCTSkip](https://developer.apple.com/documentation/xctest/xctskip)」があります。

XCTSkipを使用することで明示的に「**スキップ**」の結果を出力することができます。

特に統合テストにおいては、必要条件や依存関係が必ずと言っていいほどあるかと思います。
例えばアプリケーションがiPad特有の機能を持っている場合はiPhoneではテストできませんし古いOSでは利用できないAPIを用いる場合テスト対象は必然と絞られます。

そんな**条件付きの実行が必要なテスト**を行う際に「XCTSkip」が活躍します。

中の実装は「[こちら](https://github.com/stmontgomery/swift-corelibs-xctest/blob/405750aed2abad5840c6434442632a2cf213a48c/Sources/XCTest/Public/XCTSkip.swift)」をご参照ください。

#なぜ「スキップ」があると良いのか？

以前まで(Xcode11.4より前)は下記の2択しかありませんでした。

- テストを合格にする
- テストを不合格にする


もし一部の条件下では必ずテストが成功するようにしていた場合、**検証に合格していないプログラムが機能する**ということになりますし、テスト失敗とした場合は**何が悪いのかの判断ができず**無駄に時間を消費してしまいます。

XCTSkipを利用することでResult Bundlesを見た際にテスト結果のより詳細な情報がわかるようになりテスト全体の把握が容易になります。

Xcode11よりResult Bundlesへのデータの集約方法も変更されたのですが、そこら辺の詳細に関しては別の機会に記事を書きたいと思います。

#使い方
使い方を紹介するために簡単なサンプルを作成しました。
テキストを音声で読み上げるとてもシンプルなものです(あまり良い例が思い付かず優しい目で見てください)

```swift
import AVFoundation

class Speech {

    private var nonMaleJPVoices: [AVSpeechSynthesisVoice] {
        return AVSpeechSynthesisVoice.speechVoices()
            .filter { $0.language == "ja-JP" && $0.gender != .male }
    }
    
    private func letsSpeaking() {
        let synthesizer = AVSpeechSynthesizer()
        let utterance = AVSpeechUtterance(string:"こんにちは")
        utterance.voice = nonMaleJPVoices.first
        synthesizer.speak(utterance)
    }

}
```

`letsSpeaking()`をコールすると「こんにちは」と音声が発せられます。
`nonMaleJPVoices`では使用可能な音声オブジェクトのうち「**言語が日本語**」かつ「**男性以外**」の音声情報を渡す実装になっています。

今回は`nonMaleJPVoices`の正当性をテストしてみたいと思います。
`nonMaleJPVoices`の要件は以下の2つです。

- 言語が日本語
- 男性以外

実際に書いたテストは下記です。

```swift
class SpeechTests: XCTestCase {

    func testNonMaleJPVoicesContainOnlyJapaneseLanguage() throws {
        XCTAssertFalse(Speech().nonMaleJPVoices.contains{ $0.language != "ja-JP" }, "nonMaleJPVoices must not contain any language other than Japanese.")
    }
    
    func testNonMaleJPVoicesContainOnlyNonMaleGender() throws {
        XCTAssertFalse(Speech().nonMaleJPVoices.contains{ $0.gender == .male }, "nonMaleJPVoices must not contain a male voice.")
    }

}
```
上から順に

- `nonMaleJPVoices`は言語が日本語の要素を持つ`AVSpeechSynthesisVoice`の配列であること
- `nonMaleJPVoices`は音声の性別が男性以外の要素を持つ`AVSpeechSynthesisVoice`の配列であること

を確かめるためのテストです。
良さそうに見えるのですが、一つ注意が必要です。

`AVSpeechSynthesisVoiceGender`はiOS13より追加されたプロパティなのでそれより前のOSバージョンでは取得ができません。
iOS12では`gender`へのアクセスができないのです。

その場合テストはどうなるでしょう？

まず、機能の実装にはOSバージョンの条件分岐が入ります。

```swift
internal var nonMaleJPVoices: [AVSpeechSynthesisVoice] {
        if #available(iOS 13.0, *) {
            return AVSpeechSynthesisVoice.speechVoices()
                .filter { $0.language == "ja-JP" && $0.gender != .male }
        }
        return AVSpeechSynthesisVoice.speechVoices()
            .filter { $0.language == "ja-JP" }
    }

```

すると`男性以外`という条件を含むのは`iOS13以上`となります。

先ほどのテストに戻りますがこのままではテストとしては不十分なので修正をする必要があります。

```swift
class SpeechTests: XCTestCase {

    /// 修正が必要    
    func testNonMaleJPVoicesContainOnlyNonMaleGender() throws {
        XCTAssertFalse(Speech().nonMaleJPVoices.contains{ $0.gender == .male }, "nonMaleJPVoices must not contain a male voice.")
    }

}
```

どのように修正するのがよいでしょうか？
iOS13未満では必ずテストが成功するようにしますか？

ここで活躍するのが**XCTSkip**です。

性別に関してテストすべきなのは`iOS13以上の時だけ`でありそれより下のOSではテスト不要なので**スキップ**するのが適切です。

早速修正しましょう！

```swift
class SpeechTests: XCTestCase {

    func testNonMaleJPVoicesContainOnlyNonMaleGender() throws {
        guard #available(iOS 13.0, *) else {
            throw XCTSkip("AVSpeechSynthesisVoiceGender tests can only run on iOS 13.0+")
        }

        XCTAssertFalse(Speech().nonMaleJPVoices.contains{ $0.gender == .male }, "nonMaleJPVoices must not contain a male voice.")
    }

}
```

実際にiOS12系の端末でテストを実行してみるとこうなります。
![スクリーンショット 2020-09-19 20.45.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122157/05b448ce-0012-5ae1-d25f-eb994dffb2eb.png)
何やら見慣れないマークが表示されていますがこれがスキップしたことを示すマークです。

Report Navigatorよりスキップの詳細を見ることができます。
![スクリーンショット 2020-09-19 20.51.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122157/5c318288-b5ef-f945-4416-4282c283e6ad.png)

使い方はとても簡単ですね...！

#スキップの種類

XCTSkipを開始する関数は2種類あります。

###XCTSkipIf(_:_:file:line:)
条件式が真の場合にスキップします。

```swift
func testExample() throws
    try XCTSkipIf(UIDevice.current.userInterfaceIdiom == .pad, "this tests are not for iPad only")

    // test for other than iPad ...
}
```

###XCTSkipUnless(_:_:file:line:)
条件式が偽の場合にスキップします。

```swift
func testExample() throws
    try XCTSkipUnless(UIDevice.current.userInterfaceIdiom == .pad, "this tests are for iPad only")

    // test for iPad only...
}
```

###XCTSkip
先に関数は2種類と説明しましたが、XCTSkip構造体を直接投げることも可能です。

その場合はガードと組み合わせる使い方がおすすめでWWDC20の「[XCTSkip your tests](https://developer.apple.com/videos/play/wwdc2020/10164/)」のセッションでも同じように紹介されていました。

```swift
func testNonMaleJPVoicesContainOnlyJapaneseLanguage() throws {
        guard #available(iOS 13.0, *) else {
            throw XCTSkip("this tests can only run on iOS 13.0+")
        }

        // test for iOS13+ ...
    }
```

#まとめ
テストを書いていく中で特定の条件下では実行できないテストというのが必ずあると思います(特に統合テスト)

そんなときに**XCTSkip**を使うことでテストの実行結果をより正確にモデル化することが可能となります。

テストの結果についてより具体的に詳細を把握するためにもぜひスキップしてみてはいかがでしょうか！！