はじめに
テストを行う理由として、不具合を発見し、修正・調整できる
といったいい点があります。
コードを修正するにあたって、しっかり仕様通りに動いていることが保証されることになるため、開発をする時に安心して組むことができます。
ここでは、そういったコードのテストを書く時にどういったやり方をして行うのかを、iOSのCockooというMockテストを行うライブラリを用いて説明していきたいと思います。
単体テストとは
ここからは、単体テストを行うにつれてどういったメリットデメリットがあるのかを記載していきたいと思います。
参考文献
単体テストを実施する目的
単体テストは別名ユニットテスト・ユニット工程とも呼ばれており、名称の通り比較的小さな単位(ユニット)に対してテストを行うことで、一つの画面・機能ごとに正常な動作を担保することを目的としています。
一般的にシステム・ソフトウェア開発のプログラムは数多くの画面・機能から構成されていますが、単体テストによりユニットごとの動作が担保されることで、プログラムの組み合わせ・組み上げといった後の工程をスムーズに進めることが可能です。
単体テストを実施するメリット
- 不具合を発見し、修正・調整できる
- リファクタリングを行いやすい
単体テストのデメリット・課題点
- 準備に手間がかかる
- テストの品質はスキル次第
単体テストと結合テストの違い
単体テスト
小規模な機能単位で行われるテスト。各機能の動作確認を行い、後の工程へ繋げるのが主な目的。
結合テスト
複数のモジュールを組み合わせて行われるテスト。仕様通りにプログラムが動作することを確認するのが目的。
単体テストのやり方。
今回は、Realmを用いたTODOリストにおいて、単体テストを行ったらどのようになるのかをまとめてみたいと思います。
テストを行うアプリ紹介
メモ帳にtitleとcontentを保存して、Listに表示するアプリケーション。
CRUDのCとRは実装するが、UとDは今回は省略する。
ファイル構成
今回はMVVMの構成に従って、Model、View、ViewModelと階層を分けて実装を行なった。
![Frame 5 (3).png (150.7 kB)](https://qiita-user-contents.imgix.net/https%3A%2F%2Fimg.esa.io%2Fuploads%2Fproduction%2Fattachments%2F10655%2F2023%2F06%2F19%2F142774%2F7535910f-3432-4d0e-b290-6a410d7a3524.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=883f4c5b8b15783928a4d989c8a040a2)
- View
- ViewController
- 見た目を整形する部分
- ViewController
- ViewModel
- MainViewModel
- MainViewModelの抽象クラス
- MainViewModelImpl
- DBに渡すデータを整形したり、呼び出した後のデータを保存しておくクラス
- MainViewModel
- Model
- Memo
- DBに保存するデータクラス
- MemoRepository
- MemoRepositoryの抽象クラス
- MemoRepositoryImpl
- DBと接続するようのクラス
- Memo
![スクリーンショット 2023-06-19 21.14.24.png (126.4 kB)](https://qiita-user-contents.imgix.net/https%3A%2F%2Fimg.esa.io%2Fuploads%2Fproduction%2Fattachments%2F10655%2F2023%2F06%2F19%2F142774%2F540720c7-e8be-4105-97db-60ad155a249b.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=edfa1a4902d24b1a4f7e323371449c6f)
テストを行う内容
- MainViewModelのloadData()メソッドを呼び出した時に、正しくデータを呼び出すことができるか
- MainViewModelのcreate()メソッドを呼び出した時に、正しくデータをMemoRepositoryに送ることができるか?
- フォームに書き込むデータが足らない時にエラーを表示させることができるかどうか?
付録1 テストを行うソースコード
コードについては、様々なファイルに分かれて行なっているため、GitHubにあげました、ぜひご確認をしてください。
Cuckooをインストールする
Cuckooとは
CuckooはSwift向けに開発されたMockジェネレータです。
このライブラリを用いてMockを作成していきます。
Cuckoo は、適切な Swift モック フレームワークがないために作成されました。私たちは DSL をMockitoに非常に似たものに構築したので、Java/Android から来た人なら誰でもすぐに手に取って使用できます。
参考文献
Cuckooのインストール方法
Cuckooを使うためにCocoaPodsを用いてライブラリをインストールする。
そのために、PodFileを作成する
$ pod init
Cuckooを用いるために、Podfileを編集する
vim Podfile
pod "Cuckoo"
を追加することでライブラリが追加される
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
#
target 'DatabaseRealm' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for DatabaseRealm
pod "Cuckoo"
end
あとは、ライブラリをインストールするためにpod installをする
pod install
ファイルを開く時は、水色の方ではなく、白色の方を開くことでPodでインストールしたライブラリがうまく反映してくれる。
Testしたいファイルに結びついている依存関係のあるファイルをMock化する
Mockとは
あるクラスを擬似的に表して、ある引数がやってきた時に、こういった返り値を返すと定義づけをしたコンポーネント。
![Frame 6 (2).png (245.3 kB)](https://qiita-user-contents.imgix.net/https%3A%2F%2Fimg.esa.io%2Fuploads%2Fproduction%2Fattachments%2F10655%2F2023%2F06%2F23%2F142774%2F1b8d497f-682f-42a7-b80a-37b82bccc13f.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=87812746c89488e1d5597d543a2cb6f8)
![Frame 7 (3).png (482.0 kB)](https://qiita-user-contents.imgix.net/https%3A%2F%2Fimg.esa.io%2Fuploads%2Fproduction%2Fattachments%2F10655%2F2023%2F06%2F23%2F142774%2F71d343ed-236e-4719-b41c-5067ef38666d.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=a79e29a8ffd45c220f7d8536ced50d6d)
mockとは、一言でいうとテストに必要な部品の値を擬似的に設定するものです。
「あれ?今作ってるテストに必要なクラスBが未完成だからテストコードが作れないのでは?」
「あれ?今作ってるテストに必要なクラスBの処理が膨大すぎて変更が大変では?」と、壁にぶち当たるときがあると思います。
テストを行うクラス以外の中身をわざわざ変更するのも手間だし、変更する先のクラスが複雑で膨大な処理を行っていたら、変更、保存、実行の数だけ工数が膨れ上がります。
そんな困った問題が起きたときに使うのが"mock"です。
テストを行うクラスが他クラスのメソッドの戻り値を使用している時、mockを使うことで他クラス自体をいじらずに、その戻り値のみを自由に設定することができます。つまりテストに必要な部品を補うことができるのです。
参考文献
https://qiita.com/Fudeko/items/301f8a80963dfcaafb80
Mockを作成するために、何に依存をしているのかを調べる。
まずテストしたいファイルを開き、どのクラスを参照しているのかを調べる
下記ファイルの場合、RealmDBModelを参照しているため、このMockを作成する。
import RealmSwift
class MainViewModel{
private let realmDBModel = RealmDBModel()
コードがごにょごにょ
}
Cuckooを用いてMockを作成する。
自分のプロジェクト -> TARGETSの 自分のプロジェクトTests -> Build Phases -> RunScript
を編集する。
なかった場合は作成をする。
編集する箇所は、下の部分のGenerate mock filesと書かれている下の部分です。
そこに、作成したいファイルのパスを記載していきます。
# Generate mock files, include as many input files as you'd like to create mocks for.
"${PODS_ROOT}/Cuckoo/run" generate --testable "$PROJECT_NAME" \
--output "${OUTPUT_FILE}" \
"$INPUT_DIR/Model/RealmDBModel.swift" \
書き込み終わったら、一度適当なTestケースを走らせることで、Mockを生成してくれます。
ここの右側に表示される適当な再生ボタンみたいなものを押せばテストが走ります。
![スクリーンショット 2023-03-02 16.36.07.png (4.7 kB)](https://qiita-user-contents.imgix.net/https%3A%2F%2Fimg.esa.io%2Fuploads%2Fproduction%2Fattachments%2F10655%2F2023%2F03%2F02%2F142774%2Fa2642b71-19f8-43d7-92d2-a3196a09806b.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=c8900889f6c30495d9b1c45ad1582bf6)
生成されたら、GeneratedMocksというファイルが生成されますので、フィンダーなどからドラッグアンドドロップでXcodeに読み込ませることで、反映さえることができます。
付録 RunCodeの全体像
# Type a script or drag a script file from your workspace to insert its path.
# Define output file. Change "$PROJECT_DIR/${PROJECT_NAME}Tests" to your test's root source folder, if it's not the default name.
OUTPUT_FILE="$PROJECT_DIR/${PROJECT_NAME}Tests/GeneratedMocks.swift"
echo "Generated Mocks File = $OUTPUT_FILE"
# Define input directory. Change "${PROJECT_DIR}/${PROJECT_NAME}" to your project's root source folder, if it's not the default name.
INPUT_DIR="${PROJECT_DIR}/${PROJECT_NAME}"
echo "Mocks Input Directory = $INPUT_DIR"
# Generate mock files, include as many input files as you'd like to create mocks for.
"${PODS_ROOT}/Cuckoo/run" generate --testable "$PROJECT_NAME" \
--output "${OUTPUT_FILE}" \
"$INPUT_DIR/Model/RealmDBModel.swift" \
# ... and so forth, the last line should never end with a backslash
# After running once, locate `GeneratedMocks.swift` and drag it into your Xcode test target group.
Mockを注入するために、クラスを呼び出しているところをinit構文で行うように変更する。
今はただ、クラスが呼び出されたら、そのままインスタンスかを行うように設計をされていますが、
クラスを作成する時に、擬似的にこちらからデータを渡した場合は、そのデータを用いてインスタンスかをするように変更します。
![Frame 8 (2).png (587.1 kB)](https://qiita-user-contents.imgix.net/https%3A%2F%2Fimg.esa.io%2Fuploads%2Fproduction%2Fattachments%2F10655%2F2023%2F06%2F23%2F142774%2F28045e54-1745-4fae-bc11-5788931879f3.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=ea378642aa19ae09532aec14597c4de2)
エナジードリンクを参考にしたすごくわかりやすい例。
世界一分かりやすい「依存性逆転の原則」の説明 pic.twitter.com/hWm0AFlmSA
— ascii オプションを作った人 (@fuwasegu) February 3, 2023
変更前
import RealmSwift
class MainViewModel{
private let realmDBModel = RealmDBModel()
コードがごにょごにょ
}
変更後
import RealmSwift
class MainViewModel{
private let realmDBModel : RealmDBModel
init(realmDBModel:RealmDBModel = RealmDBModel()) {
self.realmDBModel = realmDBModel
}
コードがごにょごにょ
}
テストケースを書いていく。
とりあえずファイルを作成する。
いつも通り、ファイルを作成して、テストをかける場所を作成する。
下記ファイルはテンプレートです。
import Cuckoo
import XCTest
import RxSimport Cuckoo
import RealmSwift
import XCTest
@testable import DatabaseRealm
class MainViewModelTest: XCTestCase{
// Mockの作成
private let memoRepository = MockMemoRepository()
//終わったときに初期化する
override func tearDown() {
super.tearDown()
reset(memoRepository)
}テストを書いていく。
}
関数名test
から始まるものはtest用の関数として認識されて、テストを走らせた時に自動的に呼ばれるようになります。
主なテストの流れ
- テストするためのデータを用意する
- テストしたい関数などにデータを流し込む
MainViewModelのloadData()メソッドを呼び出した時に、正しくデータを呼び出すことができるかというテストを行う時にどのような実装をしていくのかをやってみましょう。 出力したデータが、予想された形に変わっているか確認をする。
func testReadAll(){
// テストデータの作成
let memo1 = Memo()
memo1.title = "Test Memo 1"
memo1.content = "Test Content 1"
let memo2 = Memo()
memo2.title = "Test Memo 2"
memo2.content = "Test Content 2"
let result = List<Memo>()
result.append(memo1)
result.append(memo2)
// 返り値をあらかじめ定めておく
stub(self.memoRepository) { stub in
when(stub.readAll()).thenReturn(result)
}
// 返り値を決めておいたmemoRepositoryを埋め込む
let mainViewModel = MainViewModelImpl(memoRepository: self.memoRepository)
// データを取得する
let testData = mainViewModel.loadData()
// データの検証を行う
testData.enumerated().forEach({ index , memoItem in
XCTAssertEqual(memoItem, result[index])
})
}
stubについて
Mockを用いたテストにおいて、Stubはとても重要な役割を果たします。
最初に書いた写真より、もしテストを作成したい関数に対して別のクラスの関数が用いられている時、その部分までどのような動きをするのか把握するのはとても大変になるため、こちらで、あらかじめどのような値を返すようにするのかを決め打ちで決めておく作業をします。
![Frame 7 (3).png (482.0 kB)](https://qiita-user-contents.imgix.net/https%3A%2F%2Fimg.esa.io%2Fuploads%2Fproduction%2Fattachments%2F10655%2F2023%2F06%2F23%2F142774%2Fad419fc8-48e6-4df4-85cd-d856ec4e1c55.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=e58d921289c42c9efd7d702b50f3a51a)
スタブは、関数のパラメーターとしてメソッドを呼び出すことで実行できますwhen。スタブ呼び出しは、特別なスタブ オブジェクトで行う必要があります。関数を使用して参照を取得できますstub。この関数は、スタブするモックのインスタンスと、スタブを実行できるクロージャーを受け取ります。このクロージャのパラメータはスタブ オブジェクトです。
注: 現在、サブオブジェクトがクロージャーからエスケープされる可能性があります。呼び出しをスタブするために引き続き使用できますが、この動作は将来変更される可能性があるため、実際にはお勧めしません。
関数を呼び出した後、when次のメソッドを使用して次に何をするかを指定できます。
/// Invokes `implementation` when invoked. then(_ implementation: IN throws -> OUT) /// Returns `output` when invoked. thenReturn(_ output: OUT, _ outputs: OUT...) /// Throws `error` when invoked. thenThrow(_ error: ErrorType, _ errors: Error...) /// Invokes real implementation when invoked. thenCallRealImplementation() /// Does nothing when invoked. thenDoNothing()
使用可能なメソッドは、スタブ化されたメソッドの特性によって異なります。たとえば、thenThrowメソッドは、スローまたは再スローしていないメソッドでは使用できません。
メソッドをスタブ化する例は次のようになります。
stub(mock) { stub in when(stub.greetWithMessage("Hello world")).then { message in print(message) } }
プロパティに関しては:
stub(mock) { stub in when(stub.readWriteProperty.get).thenReturn(10) when(stub.readWriteProperty.set(anyInt())).then { print($0) } }
getと に注意してくださいset。これらは後で検証に使用されます。
stubの実装方法
stub(モックかしたクラス){ stub in
when(stub.調べたいメソッド).thenReturn( 返して欲しい値 )
}
// 返り値をあらかじめ定めておく
stub(self.memoRepository) { stub in
when(stub.readAll()).thenReturn(result)
}
Matchableに対応させる
stubをそのまま実装していくと、自作の型を使用した時にこういったエラーが出てくることがあります。
エラーの原因としては、Stubでは引数として渡された値が同じでなければエラーを返す仕組みがあるのですが、自作で作られた型だと値を比べる作業ができなくてエラーになってしまうためです。
そのため、値を比べる作業ができるMatchable型に準拠させる必要があります。
Instance method 'create(memo:sucsess:failed:)' requires that 'Memo' conform to 'Matchable'
![スクリーンショット 2023-06-19 20.51.22.png (97.5 kB)](https://qiita-user-contents.imgix.net/https%3A%2F%2Fimg.esa.io%2Fuploads%2Fproduction%2Fattachments%2F10655%2F2023%2F06%2F19%2F142774%2Fb8d0c50a-caff-4eac-9d47-5516ec158bba.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=6123b72859b31e0f757adff6d051dcfb)
extension Memo:Matchable{
public var matcher: ParameterMatcher<Memo>{
return ParameterMatcher{ tested in
return self.title == tested.title &&
self.content == tested.content
}
}
}
Structで作成した場合、必要に応じてEqutableにも対応させると比べる作業がとても簡単になるのでおすすめです。
![スクリーンショット 2023-06-12 23.26.23.png (40.5 kB)](https://qiita-user-contents.imgix.net/https%3A%2F%2Fimg.esa.io%2Fuploads%2Fproduction%2Fattachments%2F10655%2F2023%2F06%2F12%2F142774%2Fd9fd0869-03aa-45bb-aad1-63c39e3052bc.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=a8df32bfc1a0eb3ef6db146b29d44112)
verifyについて
Verifyはそのメソッドが何回呼び出されたのかを把握するために使われます。
実装上で1度は呼び出されるはずなのに、間違った条件分岐によって呼び出されなかったなどを確認することができます。
参考文献
https://github.com/Brightify/Cuckoo#verification呼び出しを検証するには、 function がありますverify。最初のパラメーターはモック オブジェクトで、オプションの 2 番目のパラメーターは呼び出しマッチャーです。次に、そのパラメーターを使用した呼び出しが続きます。
verify(mock).greetWithMessage("Hello world")
プロパティの検証は、スタブ化に似ています。
function を使用したモックにこれ以上相互作用がないかどうかを確認できますverifyNoMoreInteractions。
Swift のジェネリック型では、ジェネリック パラメータを戻り値の型として使用できます。これらのメソッドを適切に検証するには、戻り値の型を指定できる必要があります。
// Given: func genericReturn<T: Codable>(for: String) -> T? { ... } // Verify verify(mock).genericReturn(for: any()).with(returnType: String?.self)
verifyの実装方法
verify(モッククラス,times(呼ばれた回数)).調べたいメソッド()
verify(self.memoRepository, times(1)).create(
memo: memoArgumentCaptor.capture(),
sucsess: successArgumentCaptor.capture(),
failed: failedArgumentCaptor.capture()
)
※timesはデフォルトだと1
verifyを用いた引数の監視方法について
関数の中で、具体的な値を持ってテストを行いたいが、最終的な出力結果がクラスのメソッドに突っ込まれて何も返してくれないどうしよぉ...となることがしばしばありました。
そういった時は、メソッドの引数として突っ込まれる値を監視してその値を元にテストを記載していく
例えば、MemoRepositoryに対してデータを送った時に、正しいデータの形に変形されているかを確認するときなど。
ようにしていきます。
func create(memo:Memo, sucsess:(_ text:String) -> Void,failed:(_ text:String) -> Void ){
if(memo.title.isEmpty || memo.content.isEmpty){
failed("空のフィールドがあります")
}
memoRepository.create(
memo:memo,
sucsess: {
sucsess("DBに保存できました")
},
failed: {
failed("DBの保存に失敗しました")
}
)
}
参考文献
https://github.com/Brightify/Cuckoo#argument-capture呼び出しの検証で引数をキャプチャするために使用できますArgumentCaptor(スタブでこれを行うことはお勧めしません)。コード例を次に示します。
mock.readWriteProperty = 10 mock.readWriteProperty = 20 mock.readWriteProperty = 30 let argumentCaptor = ArgumentCaptor<Int>() verify(mock, times(3)).readWriteProperty.set(argumentCaptor.capture()) argumentCaptor.value // Returns 30 argumentCaptor.allValues // Returns [10, 20, 30]
ご覧のとおり、 method をcapture()使用して呼び出しのマッチャーを作成し、プロパティvalueと を介して引数を取得できますallValues。value最後にキャプチャされた引数を返すか、存在しない場合は nil を返します。allValuesキャプチャされたすべての値を含む配列を返します。
Verifyを用いて引数を監視する実装方法
// Captorの取得
let memoArgumentCaptor = ArgumentCaptor<Memo>()
let successArgumentCaptor = ArgumentCaptor< () -> Void >()
let failedArgumentCaptor = ArgumentCaptor< () -> Void >()
// 返り値を決めておいたmemoRepositoryを埋め込む
let mainViewModel = MainViewModelImpl(memoRepository: self.memoRepository)
mainViewModel.create(memo: memo1, sucsess: successFunc, failed: failedFunc)
verify(self.memoRepository, times(1)).create(
memo: memoArgumentCaptor.capture(),
sucsess: successArgumentCaptor.capture(),
failed: failedArgumentCaptor.capture()
)
XCTAssertEqual(memo1, memoArgumentCaptor.value)
Verifyを使用した、テストの例
func testCreate(){
// テストデータの作成
let memo1 = Memo()
memo1.title = "Test Memo 1"
memo1.content = "Test Content 1"
// 正しい文字列が入っていることを確認した後を実装
let successFunc: (String) -> Void = { message in
// Success closure implementation
print("Success: \(message)")
}
// 間違った文字列が入っている時に呼ばれる
let failedFunc: (String) -> Void = { error in
// Failed closure implementation
XCTFail("失敗した")
}
// memoRepositoryをStubするためのデータ
let successRepoFunc = VoidClosureWrapper {
// 成功時のクロージャの実装
}
// memoRepositoryをStubするためのデータ
let failedRepoFunc = VoidClosureWrapper {
// 失敗時のクロージャの実装
}
// 返り値をあらかじめ定めておく
stub(self.memoRepository) { stub in
when(stub.create(memo: memo1, sucsess:successRepoFunc , failed: failedRepoFunc)).then{_ in }
}
// Captorの取得
let memoArgumentCaptor = ArgumentCaptor<Memo>()
let successArgumentCaptor = ArgumentCaptor< () -> Void >()
let failedArgumentCaptor = ArgumentCaptor< () -> Void >()
// 返り値を決めておいたmemoRepositoryを埋め込む
let mainViewModel = MainViewModelImpl(memoRepository: self.memoRepository)
mainViewModel.create(memo: memo1, sucsess: successFunc, failed: failedFunc)
verify(self.memoRepository, times(1)).create(
memo: memoArgumentCaptor.capture(),
sucsess: successArgumentCaptor.capture(),
failed: failedArgumentCaptor.capture()
)
XCTAssertEqual(memo1, memoArgumentCaptor.value)
}
XCTAssertとは
XCTAssertというもので、状態を比較したり、nilだった場合などを確認することもできます。
詳しい内容は下記内容などにわかりやすくまとまっているので、確認してみてください
付録 テストの内容
//
// MainViewModelTest.swift
// DatabaseRealmTests
//
// Created by haruto.makino on 2023/06/12.
//
import Cuckoo
import RealmSwift
import XCTest
@testable import DatabaseRealm
class MainViewModelTest: XCTestCase{
// Mockの作成
private let memoRepository = MockMemoRepository()
//終わったときに初期化する
override func tearDown() {
super.tearDown()
reset(memoRepository)
}
func testReadAll(){
// テストデータの作成
let memo1 = Memo()
memo1.title = "Test Memo 1"
memo1.content = "Test Content 1"
let memo2 = Memo()
memo2.title = "Test Memo 2"
memo2.content = "Test Content 2"
let result = List<Memo>()
result.append(memo1)
result.append(memo2)
// 返り値をあらかじめ定めておく
stub(self.memoRepository) { stub in
when(stub.readAll()).thenReturn(result)
}
// 返り値を決めておいたmemoRepositoryを埋め込む
let mainViewModel = MainViewModelImpl(memoRepository: self.memoRepository)
// データを取得する
let testData = mainViewModel.loadData()
// データの検証を行う
testData.enumerated().forEach({ index , memoItem in
XCTAssertEqual(memoItem, result[index])
})
}
func testCreate(){
// テストデータの作成
let memo1 = Memo()
memo1.title = "Test Memo 1"
memo1.content = "Test Content 1"
// 正しい文字列が入っていることを確認した後を実装
let successFunc: (String) -> Void = { message in
// Success closure implementation
print("Success: \(message)")
}
// 間違った文字列が入っている時に呼ばれる
let failedFunc: (String) -> Void = { error in
// Failed closure implementation
XCTFail("失敗した")
}
// memoRepositoryをStubするためのデータ
let successRepoFunc = VoidClosureWrapper {
// 成功時のクロージャの実装
}
// memoRepositoryをStubするためのデータ
let failedRepoFunc = VoidClosureWrapper {
// 失敗時のクロージャの実装
}
// 返り値をあらかじめ定めておく
stub(self.memoRepository) { stub in
when(stub.create(memo: memo1, sucsess:successRepoFunc , failed: failedRepoFunc)).then{_ in }
}
// Captorの取得
let memoArgumentCaptor = ArgumentCaptor<Memo>()
let successArgumentCaptor = ArgumentCaptor< () -> Void >()
let failedArgumentCaptor = ArgumentCaptor< () -> Void >()
// 返り値を決めておいたmemoRepositoryを埋め込む
let mainViewModel = MainViewModelImpl(memoRepository: self.memoRepository)
mainViewModel.create(memo: memo1, sucsess: successFunc, failed: failedFunc)
verify(self.memoRepository, times(1)).create(
memo: memoArgumentCaptor.capture(),
sucsess: successArgumentCaptor.capture(),
failed: failedArgumentCaptor.capture()
)
XCTAssertEqual(memo1, memoArgumentCaptor.value)
}
func testCreateNonTitle(){
// テストデータの作成
let memo1 = Memo()
memo1.title = ""
memo1.content = "Test Content 1"
// 正しい文字列が入っていることを確認した後を実装
let successFunc: (String) -> Void = { message in
// Success closure implementation
print("Success: \(message)")
XCTFail("値がないのに成功している")
}
// 間違った文字列が入っている時に呼ばれる
let failedFunc: (String) -> Void = { error in
// Failed closure implementation
print("Failed: \(error)")
}
// memoRepositoryをStubするためのデータ
let successRepoFunc = VoidClosureWrapper {
// 成功時のクロージャの実装
}
// memoRepositoryをStubするためのデータ
let failedRepoFunc = VoidClosureWrapper {
// 失敗時のクロージャの実装
}
// 返り値をあらかじめ定めておく
stub(self.memoRepository) { stub in
when(stub.create(memo: memo1, sucsess:successRepoFunc , failed: failedRepoFunc)).then{_ in }
}
// Captorの取得
let memoArgumentCaptor = ArgumentCaptor<Memo>()
let successArgumentCaptor = ArgumentCaptor< () -> Void >()
let failedArgumentCaptor = ArgumentCaptor< () -> Void >()
// 返り値を決めておいたmemoRepositoryを埋め込む
let mainViewModel = MainViewModelImpl(memoRepository: self.memoRepository)
mainViewModel.create(memo: memo1, sucsess: successFunc, failed: failedFunc)
verify(self.memoRepository, times(1)).create(
memo: memoArgumentCaptor.capture(),
sucsess: successArgumentCaptor.capture(),
failed: failedArgumentCaptor.capture()
)
XCTAssertEqual(memo1, memoArgumentCaptor.value)
}
func testCreateNonContent(){
// テストデータの作成
let memo1 = Memo()
memo1.title = "Test Memo 1"
memo1.content = ""
// 正しい文字列が入っていることを確認した後を実装
let successFunc: (String) -> Void = { message in
// Success closure implementation
print("Success: \(message)")
XCTFail("値がないのに成功している")
}
// 間違った文字列が入っている時に呼ばれる
let failedFunc: (String) -> Void = { error in
// Failed closure implementation
print("Failed: \(error)")
}
// memoRepositoryをStubするためのデータ
let successRepoFunc = VoidClosureWrapper {
// 成功時のクロージャの実装
}
// memoRepositoryをStubするためのデータ
let failedRepoFunc = VoidClosureWrapper {
// 失敗時のクロージャの実装
}
// 返り値をあらかじめ定めておく
stub(self.memoRepository) { stub in
when(stub.create(memo: memo1, sucsess:successRepoFunc , failed: failedRepoFunc)).then{_ in }
}
// Captorの取得
let memoArgumentCaptor = ArgumentCaptor<Memo>()
let successArgumentCaptor = ArgumentCaptor< () -> Void >()
let failedArgumentCaptor = ArgumentCaptor< () -> Void >()
// 返り値を決めておいたmemoRepositoryを埋め込む
let mainViewModel = MainViewModelImpl(memoRepository: self.memoRepository)
mainViewModel.create(memo: memo1, sucsess: successFunc, failed: failedFunc)
verify(self.memoRepository, times(1)).create(
memo: memoArgumentCaptor.capture(),
sucsess: successArgumentCaptor.capture(),
failed: failedArgumentCaptor.capture()
)
XCTAssertEqual(memo1, memoArgumentCaptor.value)
}
}
extension Memo:Matchable{
public var matcher: ParameterMatcher<Memo>{
return ParameterMatcher{ tested in
return self.title == tested.title &&
self.content == tested.content
}
}
}
class VoidClosureWrapper {
let closure: () -> Void
init(closure: @escaping () -> Void) {
self.closure = closure
}
}
extension VoidClosureWrapper: Matchable {
public var matcher: ParameterMatcher<() -> Void> {
return ParameterMatcher { tested in
// Add your matching logic here if needed
return true
}
}
}
まとめ
今回は、Cuckooを用いてSwiftにおけるUnitTestの行い方をまとめました。
MVVMなどのアーキテクチャに合わせて、その内容をテストすることにより、より高品質な開発を行うことができます。
副作用のあるクラスがテストに含まれている時は、Mockを用いることにより簡単に副作用を取り除くことができ、そのライブラリとしてCuckooの使い方をまとめました。
ぜひ、今回の記事を参考にして、今後のSwift開発ライフにちょっと幸があることをお祈りしております。