XCTSkipの使い方まとめ


はじめに

Xcode11.4からXCTestフレームワークに新しく導入されたプロパティの一つに「XCTSkip」があります。

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

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

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

中の実装は「こちら」をご参照ください。

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

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

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

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

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

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

使い方

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

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つです。

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

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

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バージョンの条件分岐が入ります。

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以上となります。

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

class SpeechTests: XCTestCase {

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

}

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

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

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

早速修正しましょう!

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系の端末でテストを実行してみるとこうなります。

何やら見慣れないマークが表示されていますがこれがスキップしたことを示すマークです。

Report Navigatorよりスキップの詳細を見ることができます。

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

スキップの種類

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

XCTSkipIf(:line:)

条件式が真の場合にスキップします。

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

    // test for other than iPad ...
}

XCTSkipUnless(:line:)

条件式が偽の場合にスキップします。

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」のセッションでも同じように紹介されていました。

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

        // test for iOS13+ ...
    }

まとめ

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

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

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