SwiftUIでUnit Testを行う
SwiftUIでのUnit Test
SwiftUI
にはUnit Testのためのフレームワークが標準では(今のところ)用意されていませんが、サードパティ製のフレームワーク(ライブラリ)があります。
このViewInspector
は、主にSwiftUIでUnit Testするためのライブラリです。private APIを使用していないため、プロダクションに使うこともできます。
ここではUnit Testでの使い方を紹介します。
Swift Package Managerを使ってインストール
cocoapods
やcarthage
でインストールする方法もありますが、今回はSwift Package Manager(SPM)
でインストールします。
- Xcodeプロジェクト設定>PROJECT>Swift Packages>"+"ボタンを押します
- URL入力欄に
https://github.com/nalexn/ViewInspector
を入力します - バージョンを選択し
Next
を押します -
Add to Target
としてUnit Testのターゲットを選択します
※もし、プロダクションやUI Testでも使う場合は、Link Binary with Libraries
などから適宜追加します
実装
ContentViewとUnit Testを書く
SwiftUI
のViewを書きます。今回はテキストとカウントを+1していくボタンを追加します。
struct ContentView: View {
@State var count: Int = 0
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Text("\(count)")
Button.init("Increment") {
self.count += 1
}
}
}
}
次にUnit Testを書きます。
import XCTest
import ViewInspector //1
@testable import TestSample
extension ContentView: Inspectable { } //2
class TestSampleTests: XCTestCase {
.....
func testView() throws {
try XCTContext.runActivity(named: "static string") { _ in
let view = ContentView()
let text = try view.inspect().vStack().text(0).string() //3
XCTAssertEqual(text, "Hello, 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")
}
}
}
ポイントは
-
ViewInspector
をimportする - カスタムView(今回はContentView)に
Inspectable
を準拠する -
view.inspect
を使う
です。あとはView構造に従ってText
を見つけます。
var count = try view.inspect().vStack().text(1).string()
上記では、ContentView > VStack > Textのように取得しています。最終的にText
の中の文字列を取り出し、XCTAssertEqual
で評価しています。
ViewModelを追加する
上記のContentView
の実装では、構造がかなり簡単なため@State
を使っていましたが、実際には実装の複雑さなどからViewModel
などを使うことが多いと思うので、ViewModel
で書き直してみます。
class ViewModel: ObservableObject {
@Published var count: Int
init(count: Int) {
self.count = count
}
/// カウントを+1
func increment() {
self.count += 1
}
}
struct ContentView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Text("\(self.viewModel.count)")
.tag(5) //ViewInspectorで見つけやすくするため
Button.init("Increment") {
self.viewModel.increment()
}
}
}
}
テストは以下のようになります。
import XCTest
import ViewInspector //1
@testable import TestSample
extension ContentView: Inspectable { } //2
class TestSampleTests: XCTestCase {
.....
func testViewModel() throws {
try XCTContext.runActivity(named: "static string") { _ in
let view = ContentView(viewModel: .init(count: 0))
let text = try view.inspect().vStack().text(0).string()
XCTAssertEqual(text, "Hello, world!")
}
try XCTContext.runActivity(named: "dynamic string") { _ in
let view = ContentView(viewModel: .init(count: 0))
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")
try view.inspect().find(button: "Increment").tap()
count = try view.inspect().find(viewWithTag: 5).text().string()
XCTAssertEqual(count, "2")
}
try XCTContext.runActivity(named: "dynamic string with initial value") { _ in
let view = ContentView(viewModel: .init(count: 5))
var count = try view.inspect().vStack().text(1).string()
XCTAssertEqual(count, "5")
try view.inspect().vStack().button(2).tap()
count = try view.inspect().vStack().text(1).string()
XCTAssertEqual(count, "6")
}
}
}
今回はXCTContext
を使い、テストを階層化しています。
ViewModel
を初期化する時count
を渡しており、異なる初期値のContentView
を生成しています。初期値ごとにテストをしています。
上記の例では、階層的にTextを見つける方法
var count = try view.inspect().vStack().text(1).string()
ボタンのラベル文字列から見つける方法
try view.inspect().find(button: "Increment").tap()
tagからTextを見つける方法
count = try view.inspect().find(viewWithTag: 5).text().string()
を紹介しました。他にも使用方法がいろいろあります。
サンプル
参考
Discussion