10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftUIAdvent Calendar 2022

Day 17

【SwiftUI×テスト】ViewInspectorでテスト書いてみた!(コードあり)

Last updated at Posted at 2022-11-15

目的

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))
    }
}

以上です! 気になることやアドバイスは、ぜひコメントお願いします!

参考文献

10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?