LoginSignup
12
10

More than 3 years have passed since last update.

XCTestの非同期テストで「呼び出されないこと」を検証する

Last updated at Posted at 2020-06-03

結論

XCTestExpectationのisInvertedをtrueに設定する

非同期テスト

下記のようなサブスレッドを利用して結果を非同期に返すクラスがあるとします。

class AsyncTask {
    func execute(completion: @escaping (Bool) -> Void) {
        DispatchQueue.global().async {
            completion(false)
        }
    }
}

このクラスのテストをXCTestで書く際、以下のような実装をしてしまうと、該当のテストケースは非同期処理の完了を待たずにテストを終了させてしまいます。

class SampleTests: XCTestCase {

    var asyncTask: AsyncTask!

    override func setUpWithError() throws {
        asyncTask = AsyncTask()
    }

    func test() throws {
        asyncTask.execute { result in
            // このクロージャが呼び出される前にテストが終了してしまう
            XCTAssertTrue(result)
        }
    }

}

そのため、テストケースとしてはtrueを期待しており、AsyncTaskはfalseを返しているので、本来テストは失敗して欲しいところですが、このテストは意図とは異なり成功で終了してしまいます。

XCTestExpectation

この問題を解決するには、XCTestExpectation を利用する必要があります。
これはXCTestCase内で expectation メソッドを呼び出すことで生成でき、 XCTestCaseのwaitメソッドに受け渡すことで非同期処理が完了するまでテストの終了を待機させることができます。

XCTestCaseに非同期処理が完了したことを伝えるには XCTestExpectationの fulfill を呼び出す必要があります。
また、waitメソッドのtimeoutまでにfullfillが呼び出されなかった場合はテストが失敗することになります。

先ほどのサンプルコードをXCTestExpectationを利用した形に修正すると下記のようになります。

func test() throws {
    let expectation = self.expectation(description: "wait for async task")

    asyncTask.execute { result in
        XCTAssertTrue(result)
        // 非同期処理が完了したことを知らせる
        expectation.fulfill()
    }

    // fullfillメソッドが呼び出されるまでテストを終了させずに最大0.1秒間待機する
    self.wait(for: [expectation], timeout: 0.1)
}

これによりfullfillメソッドが呼び出されるまではテストが終了されないことを保証できるので、テストがちゃんと失敗してくれるようになります。

呼び出されないことの検証

非同期テストは基本的には上記の書き方で検証をすることができますが、たまに非同期で実行され得る処理が「呼び出されない」ことを検証しておきたいケースがあります。
このテストを実装するにはどうすればよいでしょうか。

サンプルコードを少し修正して、引数によっては完了ハンドラを呼び出さずに関数を終了させるような実装に変更してみます。

class AsyncTask {
    func execute(shouldCallCompletion: Bool, completion: @escaping () -> Void) {
        if !shouldCallCompletion {
            return
        }

        DispatchQueue.global().async(execute: completion)
    }
}

テストコードは下記のようになります。

func testShouldNotCalled() throws {
    let expectation = self.expectation(description: "wait for async task")

    asyncTask.execute(shouldCallCompletion: false) {
        expectation.fulfill()
    }

    wait(for: [expectation], timeout: 0.1)
}

非同期関数の引数にfalseを受け渡しているので、この関数は完了ハンドラを呼び出さずに終了します。
そのためfulfillメソッドが呼び出されないことでwaitメソッドのタイムアウトを超過し、テストとしては失敗します。

「テストが失敗している=呼び出されていない」という判断はできるので、期待している挙動になってはいます。
しかし、自動テストとしてこの状態をOKとすることはできません。

こういった場合に利用できるのが、XCTestExpectationの isInverted プロパティです。
このプロパティにtrueを設定しておくと、テストケースは「fullfillが呼び出されない」という挙動を期待するようになります。

func testShouldNotCalled() throws {
    let expectation = self.expectation(description: "wait for async task")
+    expectation.isInverted = true

    asyncTask.execute(shouldCallCompletion: false) {
        expectation.fulfill()
    }

    wait(for: [expectation], timeout: 0.1)
}

1行設定を追加して実行すると、テストが成功するようになります。
これによって期待している「ハンドラが呼び出されなければテストを成功させる」という挙動にすることができました:tada:

注意点

この isInverted プロパティは「timeoutを超過するまでにfullfillが呼ばれなければ成功」という形でテストケースを扱うようになるため、timeoutに大きな値を設定していた場合、テストの実行時間が増加してしまう可能性があります。

そのため、必要最小限の値を設定しておくことをお勧めします。

12
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10