Open20

【Swift】テストについて学んでいく

ピン留めされたアイテム
とんとんぼとんとんぼ

What is this?

タイトルの通り、自分がテストについて無知無知なので、テストを勉強しながらメモを取っていくScrapって感じです。

あくまでメモなので、基本的に箇条書きになります。

参考文献を載せるので、興味のある方はぜひ、書籍の購入を検討してください

間違っているところや認識が違うてんがありましたらコメントください🙇‍♂️

参考文献

https://www.amazon.co.jp/増補改訂第3版-Swift実践入門-直感的な文法と安全性を兼ね備えた言語-PRESS-plusシリーズ/dp/4297112132

とんとんぼとんとんぼ

定義方法

  • 名前のメソッドはtestで始める
  • XCTestCaseクラスを継承したクラス内に記述する
  • XCTestCaseクラスを利用するにはXCTestをimportする必要がある
SampleTests.swift
import XCTest

final class SampleTests: XCTestCase {
    func testExample() {
          //  ここにテストを書く
    }
}
とんとんぼとんとんぼ

テスト対象をimport

  • モジュール外からアクセスできるコードはpublicに限られる
  • 解決するには@testable属性を利用→internalpublicと同様になる
  • Q.privateも同様か?
SampleTests.swift
import XCTest
@testable import Demo

final class SampleTests: XCTestCase {
    func testExample() {
          //  ここにテストを書く
    }
}
とんとんぼとんとんぼ

アサーションの種類

XCTestが提供するアサーションを利用する。用途はさまざまで、次の4つに分類される

  • 単一の式を評価するアサーション
  • 2つの式を比較するアサーション
  • エラーの有無を評価するアサーション
  • 無条件に失敗するアサーション

そもそも「アサーション」とは?

アサーションは、プログラムがある時点で満たしているべき条件を記述するための機能
条件が満たされていない場合はプログラムの実行を中断します。

https://qiita.com/onishi_820/items/66d65f7b56ae178b52b8#:~:text=アサーションは、プログラムがある,実行を中断します。&text=実行時エラーを発生させるのはデバッグ時,処理を継続します。

とんとんぼとんとんぼ

単一の式を評価するアサーション

アサーション関数 式の型 テストが成功する条件
XCTAssert(_:)関数 Bool 引数の式がTrueを返す
XCTAssertTrue(_:)関数 Bool 引数の式がTrueを返す
XCTAssertFalse(_:)関数 Bool 引数の式がFalseを返す
XCTAssertNil(_:)関数 Optional<Wrapped>型 引数の式がnilを返す
XCTAssertNotNil(_:)関数 Optional<Wrapped>型 引数の式がnilでない値を返す
final class SingleExpressionAssertionTests: XCTestCase {
    func testExample() {
        let value = 5
        XCTAssert(value == 5) // 引数の式がTrueを返す時、テストが成功する
        XCTAssertTrue(value > 0) // 引数の式がTrueを返す時、テストが成功する
        XCTAssertFalse(value < 0) // 引数の式がfalseを返す時、テストが成功する
        
        let nilValue = nil as Int?
        XCTAssertNil(nilValue) // 引数の式がnilを返す時、テストが成功する
        
        let optionalValue = Optional(1)
        XCTAssertNotNil(optionalValue) // 引数の式がnilでない値を返す
    }
}

XCTAssert(_:)関数とXCTAssertTrue(_:)関数の機能は同じ
→可読性に影響がでない場合はXCTAssert(_:)関数を使うと良い

とんとんぼとんとんぼ

2つの式を比較するアサーション

アサーション関数 式の型 テストが成功する条件
XCTAssertEqual(_: _:)関数 Equatableプロトコルに準拠した型 2つの結果が等しい
XCTAssertEqual(_: _:accuracy:)関数 FloatingPointプロトコルに準拠した型 2つの式の差分が第3引数で指定された値よりも小さい
XCTAssertNotEqual(_: _:)関数 Equatableプロトコルに準拠した型 2つの式の結果が等しくない
XCTAssertLessThan(_: _:)関数 Comparableプロトコルに準拠した型 第1引数<第2引数
XCTAssertLessThanOrEqual(_: _:)関数 Comparableプロトコルに準拠した型 第1引数\leq第2引数
XCTAssertGreaterThan(_: _:)関数 Comparableプロトコルに準拠した型 第1引数>第2引数
XCTAssertGreaterThanOrEqual(_: _:)関数 Comparableプロトコルに準拠した型 第1引数\geq第2引数
import XCTest

final class TwoExpressionAssertionTests: XCTestCase {
    func testExample() {
        XCTAssertEqual("abc", "abc") // 2つの結果が等しい
        XCTAssertEqual(0.002, 0.003, accuracy: 0.1) // 2つの差がaccuracyより小さい
        XCTAssertNotEqual("abc", "def") // 2つの結果が異なる
        XCTAssertLessThan(4, 7) // 第1引数 < 第2引数
        XCTAssertLessThanOrEqual(7, 7) // 第1引数 ≦ 第2引数
        XCTAssertGreaterThan(6, 4) // 第1引数 > 第2引数
        XCTAssertGreaterThanOrEqual(4, 4) // 第1引数 ≧ 第2引数
    }
}
とんとんぼとんとんぼ

エラーの有無を評価するアサーション

式がthrowキーワードによるエラーを発生させるかどうかを評価

アサーション関数 式の型 テストが成功する条件
XCTAssertThrowsError(_:) 任意 エラーが発生した
XCTAssertNoThrow(_:) 任意 エラーが発生しなかった
final class ErrorAssertionTests: XCTestCase {
    func test() {
        XCTAssertThrowsError(
            try throwableFunction(throwsError: true)
        )
        XCTAssertNoThrow(
            try throwableFunction(throwsError: false)
        )
    }
}


ここはもう少しわかりやすいサンプルコードがありそう

とんとんぼとんとんぼ

無条件に失敗するアサーション

  • XCTFail()関数は無条件に失敗するアサーション関数
  • テストが特定のパスを通らないことを保証したい場合などに使用する

以下のコードはInt?型の定数optionalValueに値が存在することを期待している。値が存在すると成功、値が存在しないと失敗

import XCTest

func test() {
    let optionalValue = Optional(3)
    guard let value = optionalValue else {
        XCTFail()
        return
    }
    // Valueを使ったテストを作成
}


あまり使い所がわからない・・・

とんとんぼとんとんぼ

テストケース

  • 個々のテストはXCTestCaseクラスを継承したクラスのメソッドとして定義される
  • テストケース:同じクラスないに定義されたテストの集まり
  • 関連するテストは同じテストケース内に配置する

テストの事前処理と事後処理

<例>
事前処理:テストの実行前にテスト用のデータを作成する
事後処理:作成したデータを削除する

共通の事前処理をsetUp()メソッド、共通の事後処理をtearDown()メソッドで定義する

とんとんぼとんとんぼ

setUP()

  • setUp()メソッドには、インスタンスメソッドとクラスメソッドの両方がある
  • インスタンスメソッドはテストごとの事前処理を行う
  • クラスメソッドはテストケース全体の事前処理を行う
import XCTest

final class SetUpTests: XCTestCase {
    override class func setUp() {
        super.setUp()
        print("テストケース全体の事前処理") //1
    }
    
    override func setUp() {
        super.setUp()
        print("テストごとの事前処理") //2
    }
    
    func test1() {
        print("テスト1") //3
    }
    
    func test2() {
        print("テスト2") //4
    }
}
  • setUp()メソッドはテストケースに対して一度だけ実行される
  • インスタンスメソッドのsetUp()メソッドは個々のテストごとに実行されているのがわかる
とんとんぼとんとんぼ

tearDown()メソッド

  • 一時ファイルの削除など、テストの後に必要な処理を実行する
  • インスタンスメソッドとクラスメソッドがある
  • インスタンスメソッドでは、テストごとの事後処理を行う
  • クラスメソッドでは、テストケース全体の事後処理を行う
import XCTest

final class TearDownTests: XCTestCase {
    override class func tearDown() {
        super.tearDown()
        print("テストケース全体の事後処理")// 5
    }
    
    override func tearDown() {
        super.tearDown()
        print("テストごとの事後処理") // 2, 4
    }
    
    func test1() {
        print("テスト1") // 1
    }
    
    func test2() {
        print("テスト2") // 3
    }
}
とんとんぼとんとんぼ

テストの実行のコントロール

  • テストケースクラスにはテストの実行をコントロールする機能がある
  • テストを中断、非同期処理を待ち合わせしたりすることができる
とんとんぼとんとんぼ

失敗時のテストの中断

  • テストの途中でアサーションが失敗しても、後続の処理は実行される
  • しかし、この挙動が問題になることがある
    • 後続の処理が事前のアサーションの結果に依存している場合
    • 実行時間に時間がかかる場合

例:Int!型の定数optionalValueの値がnilであるため、XCTAssertNotNil(_:)が失敗する。後続の処理は継続して実行されるため、optionalValue + 7の評価時の実行時エラーが発生する

import XCTest

final class SomeTestCase: XCTestCase {
    func testWithNil() {
        let optionalValue: Int! = nil
        XCTAssertNotNil(optionalValue)
        XCTAssertEqual(optionalValue + 7, 10)
    }
}

実行時にエラーが発生するとユニットテスト全体が停止してしまい、以降のテストが実行されない

これを避けるにはcontinueAfterFailureプロパティにfalseを指定する(trueにすると、test失敗しても最後まで処理する)

import XCTest

final class SomeTestCase: XCTestCase {
    func testWithNil() {
        
+        continueAfterFailure = false
        
        let optionalValue: Int! = nil
        XCTAssertNotNil(optionalValue)
        XCTAssertEqual(optionalValue + 7, 10)
    }
}
とんとんぼとんとんぼ

エラーによるテストの中断

  • テストメソッドにthrowsキーワードを付与すると、テストメソッドでエラーを発生させることができる
  • エラーが発生した時点でそのテストは中断され、失敗したとみなされる
final class FailByErrorTests: XCTestCase {
    func testThrows() throws {
        let int = try throwableIntFunction(throwsError: false)
        XCTAssert(int > 0)
    }
}
とんとんぼとんとんぼ

非同期処理の待ち合わせ

  • テストは同期的に行われるため、非同期処理が含まれる場合は、その完了を待つ必要がある

例:非同期にセットされた値を後続のアサーションで利用する。非同期処理なため、テストは失敗する

import XCTest
@testable import Demo

final class GetIntAsyncTests: XCTestCase {
    func testAsync() {
        var optionalValue: Int?
        getIntAsync { value in
            optionalValue = value
        }
        
        XCTAssertEqual(optionalValue, 4)
    }
}
  • 非同期処理を待ち合わせするには、expection(description:)メソッドを使う
    • このメソッドは、非同期処理の結果を表現するXCTestExpectationクラスの値を返す
    • この値を引数にして、wait(for: timeout:)メソッドを呼び出す
    • これにより、その場所でtimeoutで指定された時間、非同期処理の待ち合わせを行うことを宣言する
  • テスト失敗timeoutで指定した時間内にXCTestExpectionクラスの値に対してfullfill()メソッドが実行されない場合
import XCTest
@testable import Demo

final class GetIntAsyncTests: XCTestCase {
    func testAsync() {
+        let asyncExpectation = expectation(description: "aync")
        var optionalValue: Int?
        getIntAsync { value in
            optionalValue = value
+            asyncExpectation.fulfill()
        }
        
+        wait(for: [asyncExpectation], timeout: 3)
        XCTAssertEqual(optionalValue, 4)
    }
}
とんとんぼとんとんぼ

スタブ

スタブ:テスト対象の依存先を置き換え、テスト用の入力を与えるためのテクニック
スタブは主に外部システムとの連携部をテストする際に利用される

サーバとの通信を行う箇所に対するテスト内で実際に通信を行うことには、次のような問題がある

  • ネットワークの状態にテスト結果が左右される
  • サーバの状態にテスト結果が左右される
  • 実行に時間がかかる

ユニットテストはアプリケーション単体の動作を保証するためのテスト

スタブを利用して外部システムからの入力を置き換える。

とんとんぼとんとんぼ

プロトコルによる実装の差し替え

  • スタブにはさまざまな実現方法があるが、プロトコルを用いる方法が一般的

方法

  • テスト対象に依存する型のプロパティやメソッドをプロトコルとして切り出す
  • テスト対象を、依存する型とプロトコルを通じてやりとりするように書き換える
    • これをすることにより、実際に依存する型を同じプロトコルに準拠した型と自由に置き換えることができる

例:URLSessionクラスを利用してHTTPステータスコードを取得する

class StatusFetcher {
    private let urlSession: URLSession
    init(urlSession: URLSession) {
        self.urlSession = urlSession
    }
    
    func fetchStatus(of url: URL, completion: @escaping (Result<Int, Error>) -> Void) {
        let task = urlSession.dataTask(with: url) { data, urlResponse, error in
            switch (urlResponse, data, error) {
            case (_, let urlResponse as HTTPURLResponse, _):
                completion(.success(urlResponse.statusCode))
            case (_, _, let error?):
                completion(.failure(error))
            default:
                fatalError()
            }
        }
        task.resume()
    }
}
とんとんぼとんとんぼ

依存先のプロトコル化

URLSessionクラスが狙っている通信機能をスタブ化することを考える。

  • StatusFetcherクラスが使っているURLSessionクラスの機能は、URL型の値を使用して非同期的にData?型とHTTPURLResponse?型とError?型の値を取得するもの
    • 通信が成功:Data型とHTTPURLResponse型の値が存在する
    • 通信が失敗:Error型の値が存在する

このメソッドを持つプロトコルをHTTPClientプロトコルとして定義する

URLSession+HTTPClient.swift
import Foundation

extension URLSession: HTTPClient {
    public func fetchContents(of url: URL, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
        let task = dataTask(with: url) { data, urlResponse, error in
            switch (data, urlResponse, error) {
            case (let data?, let urlResponse as HTTPURLResponse, _):
                completion(.success((data, urlResponse)))
            case (_, _, let error?):
                completion(.failure(error))
            default:
                fatalError()
            }
        }
    }
}
とんとんぼとんとんぼ

プロトコルに準拠したスタブの実装

  • 通信を代用するためのスタブを定義する
  • スタブは各メソッドが任意の値を返すように実装し、テストでの利用時に結果を自由にコントロールできるようにする

例:HTTPClientプロトコルに準拠したStubHTTPClientクラスを定義し、固定のResult<(Data, HTTPURLResponse)>の値を返す実装

import Foundation
@testable import Demo

final class StubHTTPClient: HTTPClient {
    let result: Result<(Data, HTTPURLResponse), Error>
    
    init(result: Result<(Data, HTTPURLResponse), Error>) {
        self.result = result
    }
    
    func fetchContents(of url: URL, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [result] in
            completion(result)
        }
        
    }
}
とんとんぼとんとんぼ

テストにおけるスタブの使用

スタブを用いて、「HTTPクライアントが200を返した時に、StatusFetcherクラスが200を結果として返す」というテストを書く

import XCTest
@testable import Demo

final class StatusFetcherTests: XCTestCase {
    let url = URL(string: "https://example.com")!
    let data = Data()
    
    func makeStubHTTPClient(statusCode: Int) -> StubHTTPClient {
        let urlResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)!
        
        return StubHTTPClient(result: .success((data, urlResponse)))
    }
    
    func test200() {
        let httpClient = makeStubHTTPClient(statusCode: 200)
        let statusFetcher = StatusFetcher(httpClient: httpClient)
        let responseExpectation = expectation(description: "waiting for response")
        
        statusFetcher.fetchStatus(of: url) { result in
            switch result {
            case .success(let statusCode):
                XCTAssertEqual(statusCode, 200)
            case .failure:
                XCTFail()
            }
            responseExpectation.fulfill()
        }
        waitForExpectations(timeout: 10)
    }
}

これで、StatusFetcherクラスのテストを書くことができた