目的
UnitTestを行うライブラリであるViewInspectorを使い、SwiftUIでテストコード書くこと。
ViewInspector とは
UnitTest用サードパティ製のフレームワーク(ライブラリ)。SwiftUIにはUnitTestのためのフレームワークが標準では(今のところ)用意されていません。そのため、ViewInspectorを使いUnitTestを作成します。
これから何をするのか?
ViewのTextの文字列についてのテスト
や ボタンを押した時場合の挙動
のテストを実装します。
手順
⓪:任意のSwiftUIプロジェクトを作成
①:SPMを用いてライブラリ(ViewInspector)をインストール
②:Viewを実装 → コピペでOK
③:テストコード実装
④:ViewModel追加/View変更 → コピペでOK
⓪:任意のSwiftUIプロジェクトを作成
今回 SwiftUI+ViewInspectorApp02
という名前でプロジェクトを作成しました。
①:SPMを用いてライブラリ(ViewInspector)をインストール
今回、私はSwift Package Manager(SPM)
でインストールします。
- Xcodeプロジェクト設定>PROJECT>Swift Packages>"+"ボタンを押します
- URL入力欄に
https://github.com/nalexn/ViewInspector
を入力します -
Up to Next Major
を選択しNextを押します - Add to Targetとして
〜Tests
のターゲットを選択します
②:Viewを実装 → コピペでOK
ContentView.swift
ContentView.swift
import SwiftUI
struct ContentView: View {
@State var count = 0
var body: some View {
VStack {
Text("Hellow World!")
.padding()
Text("\(count)")
.padding()
Button( action: {
self.count += 1
}) {
Text("CountAddButton")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
③:テストコード実装 → コピペでOK
SwiftUI_ViewInspectorApp02Tests.swift
import XCTest
import ViewInspector // 追加①
@testable import SwiftUI_ViewInspectorApp02
extension ContentView: Inspectable { } // 追加②
final class SwiftUI_ViewInspectorApp02Tests: XCTestCase {
// ==追加③==
func test_画面に表示されている文字() throws {
try XCTContext.runActivity(named: "static string") { _ in
let view = ContentView()
let text = try view.inspect().vStack().text(0).string() // VStackの0番目に配置されている要素
XCTAssertEqual(text, "Hellow World!")
}
try XCTContext.runActivity(named: "dynamic string") { _ in
let view = ContentView()
var count = try view.inspect().vStack().text(1).string() // カウントの初期値
XCTAssertEqual(count, "0")
try view.inspect().vStack().button(2).tap()
count = try view.inspect().vStack().text(1).string()
XCTAssertEqual(count, "1") // エラー
}
}
// ========
// ↓ 元々あったプログラム
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
説明
.swift
// 1つのテストケース内でいろいろなことをやってしまった結果、
// なんのテストをしているのかがわかりづらくなることがある。
// そこで、このXCTActivityを使ってわかりやすくする
try XCTContext.runActivity(named: "static string") { _ in
}
.swift
// Unitテストを行うViewをインストタンス化
let view = ContentView()
// VStack内の子要素の文字列を取得
// ↓ Text()がVStack()の何番目の要素か明記する
let text = try view.inspect().vStack().text(0).string()
// 取得した文字列 が "Hellow World!" と一致するかテスト
XCTAssertEqual(text, "Hellow World!")
.swift
// ボタンをタップ
try view.inspect().vStack().button(2).tap()
// ボタンをタップした後の count の値をテスト
XCTAssertEqual(count, "1") // ! このままだとエラーが発生します
== エラー内容
test_画面に表示されている文字(): XCTAssertEqual failed: ("0") is not equal to ("1")
👉 ViewModelを追加し、Viewに表示するデータ(count)をViewModelで管理しましょう
④:ViewModel追加
SwiftUI_ViewInspectorApp02App.swift
SwiftUI_ViewInspectorApp02App.swift
import SwiftUI
@main
struct SwiftUI_ViewInspectorApp02App: App {
var body: some Scene {
WindowGroup {
ContentView(viewModel: .init(count: 0))
}
}
}
ContentViewModel.swift
ContentViewModel.swift
import SwiftUI
class ContentViewModel: ObservableObject {
@Published var count: Int
init(count: Int) {
self.count = count
}
func increment() {
self.count += 1
}
}
ContentView.swift
ContentView.swift
import SwiftUI
struct ContentView: View {
@ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
Text("Hellow World!")
.padding()
Text("\(self.viewModel.count)")
.padding()
Button( action: {
viewModel.increment()
}) {
Text("CountAddButton")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(viewModel: .init(count: 0))
}
}
以上です! 気になることやアドバイスは、ぜひコメントお願いします!
参考文献