[iOS]試して学ぶUnitTest[XCTest]


はじめに

今回は初めてUnitTestを学びたい、試してみたい方向けの内容となっています。
よってUnitTestとは直接関係ない用語に対しても、できる限り疑問に思われそうな内容にも触れています。(筆者がそうであったので)
よって、早々に試したいと言う方は"UnitTestを試してみる"からお読み下さい。

予備知識

UnitTestを勉強し始めると必ずTDDやCI/CDといった用語ができてきます。
ここでは予備知識として紹介させて頂きます。

TDDとは

TDDとはTest Driven Developmentの略で直訳は"テスト駆動開発"です。
テスト駆動開発とは、レッド→グリーン→リファクタリングの3工程を行い開発する手法です。以下それぞれの工程の解説です。
・[レッド]機能、要件を元に失敗するテストコードを書く
→機能を実装するコード(プロダクトコード)を書く前にテストコードを書きます。つまりテストは失敗しエラーとなります。
・[グリーン]テストが成功するコードを書く
→ここで初めて機能を実装するコードを書いていきます。"テストコードが成功するように書く"事によりゴールを明確にする事ができます。
・[リファクタリング]プロダクトコードが可読性の高いコードとなるように書き換える
→"テストが成功している=可読性が高い"とは言い辛いので、テストが成功している状態を維持しつつリファクタリングをする事でより可読性を上げ、保守生の高いコードが担保できます。

つまりTDDとは設計手法の一つと言えます。

CI/CDとは

CIとはContinuous Integrationの略で直訳は継続的インティグレーションです。
Integration(統合)とはテストを含む一連の作業の事を指します。

CDとはContinuous Deliveryの略で直訳は継続的デリバリーです。
Delivery(配達)はリリースプロセス全体の事を指します。

つまり、テストを含め、機能の追加や変更などが加わる度にビルドを行い、リリースのプロセスまでを自動化させる事で継続的に、効率良く作業を行う事ができる、設計手法の一つと言えます。

以上のように、TDD、CI/CD共に設計手法の一つですので適時、テームや会社で判断され、使用される事が想像できます。どちらも、様々な記事や本にも記載されていますが、"如何なる状況でも有効"というものではなく、常に有効な手段とは限らないのでその時、その環境に適した技術選定をしていく事が重要かと思います。

UnitTestとは

UnitTestそのものは最小単位のコンポーネント(クラス、構造体、関数など)に対して行うテストの事で、ここでは最小単位のコンポーネントに対して自動化され、繰り返し実行可能で統一的な仕組みに支えられたテストの事を指します。

今回は公式からXCTestが用意されているので今回はXCTestを使用していくものとします。

UnitTestを試してみる

今回使用するサンプル

今回は数値を2つ入力すると掛け算をして出力すると言うサンプルを作ってUnitTestを試して行きます。

環境

・ macOS Big Sur 11.5
・ Xcode : 12.5

作成方法

プロジェクトを作成する際にInclude Testsにチェックを入れます。(後から入れる事もできます→参考記事)

後から新規作成する場合

unitで検索

これでUnitTestのファイルを作成する事が出来ます。

初期の関数について

ファイルを作成した時点で初期に様々な関数が入っています。

下記はそれぞれの関数を直訳したものになります。
・setUpWithError
ここにセットアップコードを入力します。このメソッドは、クラス内の各テストメソッドを呼び出す前に呼び出されます。
・tearDownWithError
ここに分解コードを入れてください。このメソッドは、クラス内の各テストメソッドの呼び出し後に呼び出されます。
・testExample
これは機能テストケースの例です。
XCTAssertおよび関連する関数を使用して、テストで正しい結果が得られることを確認します。
・testPerformanceExample
これは、パフォーマンステストケースの例です。
・self.measure
ここに時間を測定したいコードを入れてください。

テストをしてみる

上記で紹介した様にプロジェクト初期から様々な関数が入っていますが、関数は一旦削除してテストコードを書いてみます。
テストコードは"何のテストをしているか"を分かりやすくしておく事が非常に重要です。
その為には
・テストケース名を分かりやすくする
・テストコードの中身を分かりやすくする
・テストが失敗した時の情報を用意する
の3点を重視する事は様々な良書で語られていますが、ここでは特に"テストコードの中身を分かりやすくする"について解説を交えながらテストを行います。

サンプルコード

今回は数値を2つ入力すると掛け算をして出力すると言うサンプルを作ってUnitTestを試していきます。
まずは以下のModel(ロジック)を用意します。

import Foundation

protocol MultiplyInterface {
    func multiply(num1: Int, num2: Int) -> Int
}

class MultiplyModel: MultiplyInterface {
    private var num1: Int = 0
    private var num2: Int = 0
    // num1とnum2を乗算した値を返す
    func multiply(num1: Int , num2: Int) -> Int {
        let multiplication = num1 * num2
        return multiplication
    }
}

上記のModelを対象に"テストコードの中身を分かりやすく"テストを書いきます。
コードは以下の様にXCTestCaseの中に書きます。

import XCTest
@testable import UnitTestSampler_Multiply

class UnitTestSampler_MultiplyTests: XCTestCase {
    func test_num1とnum2の引数が掛け算されていれば成功() {
        // 前処理
        let multiplyModel = MultiplyModel()
        // 実行
        let result = multiplyModel.multiply(num1: 10, num2: 5)
        // 検証
        XCTAssertEqual(50, result)
        // 後処理
    }
}

シンプルなサンプルなので、テストもシンプルです。
コードについては上から順に、以下解説します。

import XCTest

XCTestフレームワークをインポートします。このフレームワークが無いと以下にあるXCTestCaseクラスやXCTAssertなどの関数を使用する事ができないので必須です。

@testable import UnitTestSampler_Multiply

今回テストを行うプロジェクトをテスト用にインポートします。つまり今回はUnitTestSampler_Multiplyと言う名前のプロジェクト内にあるコンポーネントに対してテストを行いますのでテスト用にインポートする必要があります。こちらもXCtestを用いてUnitTestする際は必須です。

class UnitTestSampler_MultiplyTests: XCTestCase {

}

テストクラスを宣言します。その際XCTestCaseに準拠させる必要があります。
テストコードはこのクラス内に書きます。

    func test_num1とnum2の引数が掛け算されていれば成功() {

    }

テストを行うための関数です。関数名はこのテストがなんのテストをしているか、関数名で分かるように書く事が良いとされています。これには明確な理由があります。テストは失敗すると、どのテストが失敗したか"メソッド名"が記載されます。コメントとして残しておく事も良いとは思いますが、失敗した際にどのテストが失敗したのか、素早く特定する為にも分かりやすい命名をしておく事はマストと言えます。命名に関してはチームや会社で取り決めがある、もしくは共通の取り決めをしておく必要がありますが、基本的に"分かりやすく"が最重要となりますので、日本語を含ませるケースもよくあります。(筆者が知る限り)
また頭にはtestをつける事でtestメソッドの対象になりますので(左側にひし形のマークが付きます)必ず頭にtestをつける必要があります。

        // 前処理
        let multiplyModel = MultiplyModel()
        // 実行
        let result = multiplyModel.multiply(num1: 10, num2: 5)
        // 検証
        XCTAssertEqual(50, result)
        // 後処理

テストではいかにテストコードの中身が分かりやすく書かれているかは重要です。
分かりやすく書く為には
・前処理
・実行
・検証
・後処理
の4つにテストコードを分けて書きます。
前処理や後処理は
・setUpWithError(前処理)
・tearDownWithError(後処理)
に書く事もあります。今回はシンプルなサンプルな為使用はしませんでしたが、APIを利用したアプリで、スタブやスパイなどを使用する際や、ローカルDB(UserDefaultsやCoreData)を使用する際はテスト後に元に戻す必要がありますので、後処理に記載して元に戻す作業を行なったりします。
実行では実際にテストを行いたいメソッドなどを実行します。
そして検証ではその実行結果を元に検証していきます。
検証で使用されるメソッドをAssert(アサート)と言います。XCTestで検証をする際は必須になりますので是非いろんなAssertがある事を覚えておくと良いかと思います。
以下Assertメソッドを一覧にしてまとめている記事がありますので是非ご活用下さい。(私も非常に活用させて頂いています。)

何をテストするか選択する方法

ここまで読んで頂きありがとうございます。この辺りでUnitTestについて大まかに理解が進んできたのでは無いでしょうか?ここからは筆者がテストを学び始めて少し経った頃に非常に困った内容を中心に解決した方法も併せてご紹介したいと思います。

何をテストすればいいかわからない

テストの書き方が少し分かってきて、嬉しく、今まで作ってきたサンプルを引っ張り出してテストを書いていると"あれ?こんなテストもあった方がいいような、、、"とか"全ての関数に対してテスト書いた方がいいのか?"とか"入力の値に対してどこまでテスト書けばいいんだ、、、"などに直面するケースがあると思います。
そもそもテストに時間やコストをかける事あまり良くないとされています。様々な理由がありますが特に多く謳われているのが、時間やコストが高いと継続的にテストがされなくなる点です。UnitTestはあくまで機能を担保するものであるので必要な部分だけあれば良いと思っています。
ではその"必要"というものは何を基準にすれば良いのか?
もちろん主要なコンポーネントに対してテストを行う事は重要ですが(今回の場合掛け算をするメソッドが重要なコンポーネントとなるので引数を2つ渡す事で掛け算がされるかテストを行なった)それ以外にもテストをしようとなった際、役立つのが以下の3つの手法です。
・同値分割
例えば点数を入力しその点数によって"優"、"良"、"可"、"不"と判断するメソッドがあったとします。
80点以上は優である場合、80〜100点の21個の数字全てをテストするべきかと考えた場合、そうではなく、代表の点数を一つ選択し、その点数をテストの値として用いれば良いとされています。
・境界値
先程の同値分割で記載した例のような場合で以下の様な条件であると仮定します。
・80点以上は優
・60点以上79点以下は良
・30点以上59点以下は可
・29点以下は不
この場合、優の境界値は80,良の境界値は79と60になります。
優の場合は
score >= 80
だと80以上となりますが、万が一
score > 80
となっていた場合81からとなります。この様に判定の境界に当たる値のみをテストするだけで、境界値がしっかり機能しているかテストする事ができます。
・単項目チェック
例えば上記の条件に
・18歳未満はそれぞれに5点足した値を基準とする
とした場合、また+5点した基準のテストを全て行いそうですが、上記の同値分割と境界値テストによって
・点数によって出力が変更されている
・境界値でも機能している
事が担保されているので、18歳未満が70点の時、良になる事が担保されるテストを書けば18歳未満も機能していると言えます。
この様にケースバイケースではありますが、単項目チェックをする事で無闇にテストを増やす事を回避するができます。

依存コンポーネントを差し替える

色々とテストを試していると次に直面するのは依存が強いクラスに対するテストが書けないという問題です。
例えばMVVMを採用して開発した場合ViewModelとModelは依存しがちです。
MVVMの詳細はここでは避けますが、ViewModelはViewとModelの架け橋となる為です。
ではどうしたら良いか。DIを注入する事です。(依存性注入)
DIについてはこちらの記事で非常にわかりやすくまとめられていますのでご覧下さい。

DIを注入したらテストにも細工をしていきます。色々と手法がある様ですが主な手法としては以下2つです。
※以下テスト対象とするコンポーネントに対してstubと差し替えられる様、共通のプロトコルを宣言し、準拠させる必要がありますのでDIは必須となります。

stub(スタブ)

例えばGitHubやQiitaなどのAPIを利用して値を取得するclassがあったとします。
このコンポーネントをそのままテストしようとすると、サーバー側に依存して不安定なテストとなります。(筆者は大事な場面でミスした事があります)
ではサーバーに依存なくテストを書くにはどうしたら良いのか?そこでstubの出番です。
stubはテスト検証をする際、テストの都合に良い値を返すテスト用のコンポーネントに差し替えて使用するものです。
例えば上記のようにサーバーに依存する様なテストではstubを差し込み、予めサーバーからの返答が成功か失敗か決めておく事ができます。すると、テストも成功のケースと失敗のケースが安定した状態で書ける状態になります。stubに関してはまた事例を用いて書きたいと思いますが、今回は入門記事なのでサンプルコードは割愛させて頂きます。

mock(モック)

外部APIを利用して値を取得する場合サーバーに格納されている情報は時間の経過と共に変化する事が多いと思います。その様な状態でXCTAssertEqualでテストを行うとどうなるでしょうか。時間と共に結果が変化するので、ある時は成功するかもしれませんが、ある時は失敗するかもしれません。これでは安定したテストとは言えません。そんな時はmockの出番です。メソッドなどで外部サーバーから値を取得する代わりに事前にmockを用意しておくことで差し替えて、安定した内容を検証する事ができます。こちらもstub同様、また事例を用いて書きたいと思いますが、今回は割愛させて頂きます。

まとめ

いかがでしたでしょうか?テストについて今まで何となく聞いたことあると言う認識から少しでも理解して頂けたでしょうか?もし、"この辺りが分かりにくい"とか"この辺りもっと詳細に知りたい"と思われた方は是非下記の参考記事や本の購入をおすすめします!!(私のTwitterにDMして頂いても構いません)
テストはとにかく書いてみると非常に面白く感じる部分もありますので是非簡単なものから書くことをおすすめします!!
以上です。最後まで読んで頂きありがとうございました!!

参考文献+オススメ記事(本)