🙄

SwiftUI + MVVM における async/await を使った ViewModel のテスト実装例(Swift Testing)

に公開

概要

iOS/Swift で async/await を非同期処理に使用している場合での SwiftUI + MVVM パターンでのテストコードの例を紹介します。
Swift Testing を使用しています。

テスト対象

テスト対象の ViewModel のコードです。
ユーザー情報をAPIから取得してユーザー名を画面に表示する処理を想定します。
エラー時には固定文言を表示します。

class UserViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var errorMessage: String = ""
    @Published var isShowError: Bool = false

    private let userModel: UserModelProtocol

    init(userModel: UserModelProtocol) {
        self.userModel = userModel
    }

    @MainActor
    func fetchUser() async {
        do {
            let user = try await userModel.fetchUser()
            self.name = user.name
        } catch {
            self.errorMessage = "エラーが発生しました"
            isShowError = true
        }
    }
}

Entity

Entityは下記の通りです。
インスタンスの作成の簡略化のために create() メソッドを実装しています。

struct User {
    let id: Int
    let name: String
}

extension User {
    static func create(id: Int = 1, name: String = "") -> Self {
        User(id: id, name: name)
    }
}

Model

async メソッドで API からユーザー情報を取得することを想定しています。
Mock を注入してテストしやすくするために、Model の protocol を ViewModel は持ちます。

protocol UserModelProtocol {
    func fetchUser() async throws -> User
}

Mock

テスト用に注入する Mock クラスは下記になります。
Mock はライブラリで自動生成されるものを使用する場合も多いですが、今回は簡単な自作の実装を使用します。
init 時にユーザー情報とAPIからの情報取得を失敗させるかのフラグを渡します。

class MockUserModel: UserModelProtocol {
    private let user: User
    private let shouldFail: Bool
    private let error: Error = NSError(domain: "TestError", code: 1)

    init(user: User = User.create(), shouldFail: Bool = false) {
        self.user = user
        self.shouldFail = shouldFail
    }

    func fetchUser() async throws -> User {
        if shouldFail {
            throw error
        }
        return user
    }
}

テストコード

実際のテストコードは下記になります。
各テストメソッドでモックを作成し、テストしたいメソッドを実行後に想定どおりかどうかを検証します。

struct UserFetchTestTests {
    @Test
    func fetchUser_Success() async throws {
        let mockUser = User.create(name: "test")
        let mockUserModel = MockUserModel(user: mockUser)
        let viewModel = UserViewModel(userModel: mockUserModel)

        await viewModel.fetchUser()

        #expect(viewModel.name == "test")
    }

    @Test
    func fetchUser_Failure() async throws {
        let mockUserModel = MockUserModel(shouldFail: true)
        let viewModel = UserViewModel(userModel: mockUserModel)

        await viewModel.fetchUser()

        #expect(viewModel.name == "")
        #expect(viewModel.isShowError == true)
        #expect(viewModel.errorMessage == "エラーが発生しました")
    }
}

1つ目のテストケースでは、ユーザー情報の取得成功時にユーザー名が正しく反映されていることを検証しています。
2つ目のテストケースでは、ユーザー情報の取得失敗時にエラーメッセージの文言とエラー表示フラグが反映されていることを検証しています。

おわりに

async await を非同期処理に使用しているViewModelのテストコードを紹介しました。
特に複雑な実装は必要なく、かなり簡単に実装できました。
また、swift testing を触るのも初めてでしたが、シンプルに実装ができるのが良いと感じました。
今後も積極的に使用していきたいです。

株式会社ソニックムーブ

Discussion