本記事の内容
実行環境
Swift | 5.6.1 |
Xcode | 14.0 |
macOS | 12.6 |
stubとmock
stubとmockとは?
stubとmockは、安定したテストを実現するために使います。
サーバーを介したテストする際、下のような課題が出てきます。
・通信の成功ケース・失敗ケースなど、ケースごとのテストが難しい
・時間によってレスポンスの状態が変化することもあるため、あるときは成功したり失敗したり、不安定なテストになるかも
この課題の
1点目をstubで解決し、2点目をmockで解決します。
僕の考えだと下の図のようなイメージです。次でstubとmockに分けて詳しく紹介します。
stubとは?
一言で表すとすれば、“ダミーの呼び出し先の処理” です。
stubに関しては下のように書かれている記事がありました。
テスト用に用意した、まだ完成していない機能の代わりとなる部品であり
スタブとは?(「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典)
テスト対象から呼び出される部品の代わりとなるやつです。
ちょっと小難しい表現を使うと下位モジュールの代用品と言えます。
サーバーに依存なくテストを書くにはどうしたら良いのか?そこでstubの出番です。
[iOS]試して学ぶUnitTest[XCTest](Qiita)
stubはテスト検証をする際、テストの都合に良い値を返すテスト用のコンポーネントに差し替えて使用するものです。
要するに、
先程の図で示したように、
テスト環境で安定したテストが行えるように、差し替える部品
と言えるでしょう。
通信の成功ケース・失敗ケースに関するテストが難しいことが、サーバーを介したテストする際の課題でした。
stubを使うことで、
都合が良いように失敗ケースをテストしたり、
それぞれのケースを想定した安定したテストを実装できます。
mockとは?
一言で表すとすれば、“ダミーのデータ” と言えるかと思います。
mockに関しては、下のように書かれている記事がありました。
Mockとは、簡単に言うとクラスの動作をシミュレートするためのオブジェクトです。テスト対象クラスが呼び出している(=依存している)クラスをMockで差し替え、Mockの動作内容を定義することで、望むテスト条件を容易に作ることができます。
Mockでユニットテストを簡単にしよう!
自動化されたユニットテストにおいて、テスト対象オブジェクトが呼び出し先のオブジェクトと意図したとおりに協調動作するかどうかを検証するために、呼び出し相手に換えて使用するテスト用のオブジェクトのこと。
情報システム用語事典 モックオブジェクト(もっくおぶじぇくと)
要するに、テスト用に準備したサーバーのレスポンスデータといえるのではないでしょうか。
先程も述べたように、
サーバーによっては時間によってレスポンスの状態が変化することもあるため、あるときは成功したり失敗したり、不安定なテストになることがテストの課題でした。
mockを使うことで、
テストの際だけレスポンスを固定できるため、安定したテストを実装できます。
作るサンプルアプリ
デモ動画
詳細
前回の記事で、QiitaAPIから最新記事タイトル20件を取得し、List表示させるミニアプリを作りました。
今回はこのミニアプリを利用して、stubとmockを使った通信のUnitテストを実装してみます。
こちらのGitHubにソースコードを載せています。
前回のソースコードを取得したい方は、
下の記事内にGitHubのリポジトリをアップしているのでご覧下さい。
サンプルアプリ実装
先程の図と重ねると、下のようなリポジトリ構成と命名で作成しました。
今回はSwiftUIのコードも混ざっていますが、UIKitでもほぼ同じコードになると思います。
通信のプロトコルを定義
このプロトコルを後ほど、
本番環境の処理である ArticleListClient.swift と
テスト環境の処理である MockArticlesAPIClient.swift に準拠させます。
protocol ArticleListAPIClientProtocol {
func fetch(completion: @escaping ((Result<[Article], APIError>) -> Void))
}
本番環境で通信を行う処理を実装
import Foundation
class ArticleListAPIClient: ArticleListAPIClientProtocol {
func fetch(completion: @escaping ((Result<[Article], APIError>) -> Void)) {
guard let url = URL(string: "https://qiita.com/api/v2/items") else {
return completion(.failure(.invalidURL))
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { (data, response, error ) in
do {
guard let data = data else { throw APIError.noneValue }
guard let articleList = try? JSONDecoder().decode([Article].self, from: data) else {
throw APIError.noneValue
}
DispatchQueue.main.async {
completion(.success(articleList))
}
} catch {
if error as? APIError == APIError.networkError {
completion(.failure(.networkError))
} else if error as? APIError == APIError.noneValue {
completion(.failure(.noneValue))
} else {
completion(.failure(.unknown))
}
}
}.resume()
}
}
テストで使うエラータイプを定義
このenum型は無くても良いのですが、
通信でエラーが発生した際に原因を特定しやすくするために実装しています。
enum APIError: Error {
case noneValue
case invalidURL
case networkError
case unknown
var title: String {
switch self {
case .noneValue:
return "値が空で取得されたエラー"
case .invalidURL:
return "無効なURLのエラー"
case .networkError:
return "ネットワークエラー"
default:
return "不明なエラー"
}
}
}
ArticleViewModelを定義
class ArticleViewModel: ObservableObject {
@Published var articles: [Article] = [Article]()
var apiError: APIError?
private let articleListAPIClient: ArticleListAPIClientProtocol!
// テスト用のイニシャライザ
init(fetchArticlesAPIClient: ArticleListAPIClientProtocol) {
articleListAPIClient = fetchArticlesAPIClient
loadArticles()
}
init() {
articleListAPIClient = ArticleListAPIClient()
loadArticles()
}
func loadArticles() {
articleListAPIClient.fetch(completion: { [weak self] resulte in
switch resulte {
case .success(let articleList):
self?.articles = articleList
case .failure(let error):
self?.apiError = error
print(error)
}
})
}
}
stubとmockの作成
下のようにArticleListAPIClientProtoloに準拠させたクラスを作ると、
stubsを作りますか?と聞かれると思うので、自動で作ってもらいましょう。
class MockArticlesAPIClient: ArticleListAPIClientProtocol {
var returnArticles: [Article]?
lazy var fetchResult: Result<[Article], APIError> = .success(mockArticles)
// mock
let mockArticles: [Article] = [
Article(title: "記事0"),
Article(title: "記事1"),
Article(title: "記事2"),
Article(title: "記事3"),
Article(title: "記事4"),
Article(title: "記事5"),
Article(title: "記事6"),
Article(title: "記事7"),
Article(title: "記事8"),
Article(title: "記事9"),
Article(title: "記事10"),
Article(title: "記事11"),
Article(title: "記事12"),
Article(title: "記事13"),
Article(title: "記事14"),
Article(title: "記事15"),
Article(title: "記事16"),
Article(title: "記事17"),
Article(title: "記事18"),
Article(title: "記事19"),
]
// stub
func fetch(completion: @escaping ((Result<[Article], APIError>) -> Void)) {
completion(fetchResult)
switch fetchResult {
case .success:
returnArticles = mockArticles
default:
returnArticles = nil
}
}
}
テストコード
import XCTest
@testable import MiniApp113_SwiftUI_stubmock02
final class MiniApp113_SwiftUI_stubmock02Tests: XCTestCase {
private var mockArticlesAPIClient: MockArticlesAPIClient!
private var articleViewModel: ArticleViewModel!
override func setUp() async throws {
mockArticlesAPIClient = MockArticlesAPIClient()
articleViewModel = ArticleViewModel(fetchArticlesAPIClient: mockArticlesAPIClient)
}
func test_記事が正常に取得できている() {
XCTAssertNotNil(articleViewModel.articles)
XCTAssertEqual(articleViewModel.articles[0].title, "記事0")
XCTAssertEqual(articleViewModel.articles[17].title, "記事17")
}
func test_通信エラーだとnetworkErrorを返す() {
mockArticlesAPIClient.fetchResult = .failure(APIError.networkError)
articleViewModel = ArticleViewModel(fetchArticlesAPIClient: mockArticlesAPIClient)
XCTAssertEqual(articleViewModel.apiError, APIError.networkError)
}
func test_値が空だとnoneValueを返す() {
mockArticlesAPIClient.fetchResult = .failure(APIError.noneValue)
articleViewModel = ArticleViewModel(fetchArticlesAPIClient: mockArticlesAPIClient)
XCTAssertEqual(articleViewModel.apiError, APIError.noneValue)
}
func test_無効なURLだとinvalidURLを返す() {
mockArticlesAPIClient.fetchResult = .failure(APIError.invalidURL)
articleViewModel = ArticleViewModel(fetchArticlesAPIClient: mockArticlesAPIClient)
XCTAssertEqual(articleViewModel.apiError, APIError.invalidURL)
}
func test_不明なエラーだとunknownを返す() {
mockArticlesAPIClient.fetchResult = .failure(APIError.unknown)
articleViewModel = ArticleViewModel(fetchArticlesAPIClient: mockArticlesAPIClient)
XCTAssertEqual(articleViewModel.apiError, APIError.unknown)
}
}
まとめ
本記事はstubとmockに関して初学者がまとめました。
出来る限り事実確認は行ったつもりですが、間違いがある可能性もあることをご了承下さい。間違いやアドバイス等は、本記事の末尾にあるコメント欄から是非お願いいたします…!
ということで、stubとmockを使って通信のUnitテスト書いてみました。
最後まで読んでいただきありがとうございました!
参考文献+オススメ記事
>> Swiftの型のMock化パターンまとめ(Qiita)
>> iOSアプリでMockを使ってUnitTestを書く(Qiita)
>> [swift]API通信を利用しているクラスをUnitTestしてみる[stubとmockの使い方](Qiita)
>> SwiftにおけるMockライブラリの活用/swift-mock-library
>> [iOS]試して学ぶUnitTest[XCTest](Qiita)
作業効率がグッと上がるPC道具
間違いなしのSwift書籍2冊