アイリッジ開発者ブログ

アイリッジに所属するエンジニアが技術情報を発信していきます。

XCTUnwrapを使ってテストコードをシンプルに

こんにちは、開発部第1グループの西岡です。

iOS開発で手軽に活用できるTipsについて紹介しています。

今回、私が紹介したいのはテストコードを書く時に便利なXCTestの関数です、

XCTestといえば

 iOS開発における一般的なユニットテストといえばXCTestフレームワークですね。XCTestとは、テスト実行から結果のフィードバックまでを統一的な仕組みとして、Appleが標準で提供してくれるUnitTestフレームワークです。

Optionalが付着している

 テストを書いていると冗長になりがちなのがオプショナルの取り扱いです。1つのテストケースの中で生成・出力されたオブジェクトが nil なのか、non-nil か、これらも重要なテスト対象なので、XCTestでは2種類の標準関数が用意されています。

Nil条件

func XCTAssertNil(() -> Any?, () -> String, file: StaticString, line: UInt)

Nilではない条件

func XCTAssertNotNil(() -> Any?, () -> String, file: StaticString, line: UInt)

それぞれのテストケースでの条件に応じて使い分けます。

冗長なアンラップ

 一方で、テストケースを書いていると自明なnilを扱うようなときがあります。テスト対象の出力仕様とは直接無関係なnilであったり、テスト用の入力値を用意する過程でオプショナルで定義せざるを得ないようなケースです。

 このようなオプショナルに対しては、XCTAssertNil()XCTAssertNotNil()の結果自体が特に重要な意味をもちません。そのため、大抵の場合だとguard-elseでfaillureになるよう誘導したり、形式的にXCTAssertNotNil()を用意した上で想定外なNGケースを拾ったりするなど、色々な方法がありそうですが、いずれにしてもアンラップするために余計なコードが増えてしまいます。

GuardとXCTFailの例

func testGenerateToken() throws {
        let user = User()

        guard let user = try user,getAuthenticationToken() else {
                XCTFail("Failure to generate token.")
                return
        }

        XCTAssertEqual(token, "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b")
}

XCTAssertNotNilの例

func testGenerateToken() throws {
        let user = User()
        let token = try user.getAuthenticationToken()

        XCTAssertNotNil(token, "Failure to generate token.")
        XCTAssertEqual(token, "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b")
}

前者のようなテストコードを毎回書くのは正直面倒です。後者の場合でも「オプショナルのままでテストするのは…」「せめてアンラップされた値でテストするべきだ…」という意見もあります。そして、仮にテスト対象の内部仕様と全く関係がないnilが混入した場合、複数箇所でassertionが発生してしまったり、問題箇所を特定しづらいなど、やはり難があります。

そこで紹介したいのが XCTUnwrap() という関数です。

XCTUnwrap関数はどうか

developer.apple.com

Xcode11から提供されるようになった、XCTestの関数です。

関数の主な特長は2点です。

  • オプショナル型の変数をUnwrapして返す
  • 例外発生時はテストを失敗にします

なんといっても1番の特長はアンラップされた値が返却されることかと思います。

func XCTUnwrap<T>(
    _ expression: @autoclosure () throws -> T?,
    _ message: @autoclosure () -> String = "",
    file: StaticString = #filePath,
    line: UInt = #line
) throws -> T

XCTUnwrap() を使うと先程のテストケースは以下のようになります。

func testGenerateToken() throws {
        let user = User()
        let token = try XCTUnwrap(user.getAuthenticationToken())

        XCTAssertEqual(token, "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b")
}

どんなassertionが発生するのでしょうか?

試しにテストを実行してみました。

tokenがnilだった場合の結果がこちらです↓

nilが返却されたとき

アンラップ時にnilが検出された場合には、独自のassertion「failed: expected non-nil value …」を発するようです。

例外が発生した場合の結果がこちら↓

例外が発生した時

例外が発生した際にはthrowされたエラー内容が表示されます。

また、UserErrorはLocalizedErrorプロトコルでカスタマイズしていたのですが、errorDescriptionのメッセージも赤く強調されて出力されます。


※ちなみに余談ですが、XCTUnwrapなどのXCTestのアサート関数ではなく、テストケースのメソッド側にエラーがthorwされるとassertionの見え方が若干異なります。

XCTUnwrap無しで例外を発生させたとき

まとめ

  • XCTUnwrap関数を用いることでオプショナル型の変数をアンラップするコードを省略することができる。
  • XCTUnwrap関数は、XCTAssertNotNilやXCTAssertEqualなどのAssert関数と同じようにテストのフィードバックを得られる。
  • もしアンラップ時にnilが検出された場合、XCTUnwrap関数が独自のassertionで知らせてくれる。