ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション(LDI)の技術ブログです

(iOS)Quick/NimbleのテストコードをRxSwiftからSwift Concurrencyへ置き換え

こんにちは、LDIのiOS/Android開発マネージャーの阪口です。

LDIで開発しているiOSアプリでは、RxSwift から Combine や Swift Concurrency への置き換えを進めています。

今回は、Quick/Nimble を利用したテストコードを RxSwift のコードから Swift Concurrency へ置き換える方法や注意点についてご紹介させていただきます。

テストコードを RxSwift から Swift Concurrency へ置き換え

Quick 7.0.0から Swift Concurrency の async/await に対応した、AsyncSpec が追加されています。

github.com

Quick 7.0.0 added AsyncSpec for defining tests of behavior utilizing async/await. This is a change from the behavior in Quick 6.

この AsyncSpec を利用して、テストコードを RxSwift から Swift Concurrency へ置き換えます。

Before:RxSwift利用時のテストコード

final class HogeModelTest: QuickSpec {
    override class func spec() {
        var repository: HogeRepositoryMock!
        var target: HogeModel!

        beforeEach {
            repository = HogeRepositoryMock()
            target = HogeModel(repository: repository)
        }

        describe("状態の取得") {
            var resultState: HogeState?
            beforeEach {
                resultState = nil

                // Arrange
                repository.state = .normal
            }
            it("状態が返却されること") {
                // Action
                _ = target.getState()
                    .subscribe(onSuccess: { resultState = $0 })

                // Assertion
                expect(resultState).toEventually(equal(HogeState.normal))
            }
        }
    }
}

After:Swift Concurrency 置き換え後のテストコード

final class HogeModelTest: AsyncSpec {
    override class func spec() {
        var repository: HogeRepositoryMock!
        var target: HogeModel!

        beforeEach {
            repository = HogeRepositoryMock()
            target = HogeModel(repository: repository)
        }

        describe("状態の取得") {
            beforeEach {
                // Arrange
                repository.state = .normal
            }
            it("状態が返却されること") {
                // Action
                let resultState = await target.getState()

                // Assertion
                expect(resultState).to(equal(HogeState.normal))
            }
        }
    }
}

置き換え時にやったこと

  1. テストクラスの親クラスを、QuickSpec から AsyncSpec へ変更
  2. spec クラスメソッドのスコープより内側のスコープの変数を削除(describe の resultState)
  3. テスト結果の取得を Swift Concurrency 用のコードに修正
  4. Nimble の toEventually を to に変更

置き換え時の注意点

https://github.com/Quick/Quick/blob/main/Documentation/en-us/AsyncAwait.md#thread-safety-concerns

What has changed is that there's no guarantee that test code used in different beforeEach, afterEach, aroundEach or it elements will run on the same thread. That is, you can't even guarantee that the following tests will pass:

とあるように、beforeEach や it はそれぞれ同一スレッドの保証はされていません。

各スコープ内で変数を定義し値を操作すると期待しない動作に繋がる可能性があります。

そのため、describe や context の スコープ内で定義した変数を、beforeEach と it それぞれで操作を実施しないように注意が必要です。

基本的には 、AsyncSpec では describe や context の スコープ内での変数定義は避けることが安全そうです。

置き換えNG例
        describe("状態の取得") {
            var resultState: HogeState?
            beforeEach {
                resultState = nil // NG

                // Arrange
                repository.state = .normal
                // Action
                resultState = await target.getState() // NG
            }
            it("状態が返却されること") {
                // Assertion
                expect(resultState).to(equal(HogeState.normal)) // NG
            }
        }

まとめ

Quick の AsyncSpec を活用することで、 Swift Concurrency の テストコードが簡単に書くことができます。

また、async/await の長所である、非同期処理の流れが分かりやすい、直感的なコードをテストコードでも実現できます。

そのため、Swift Concurrency に置き換えることで RxSwift に比べて学習コストが低く、さらに可読性の向上に繋がることが期待されます。