Sponsor Link
環境&対象
- macOS Catalina 10.15.7
- Xcode 12.2
- iOS 14.2
ViewModel の準備ができたところで、View を作っていきます。
View の実装
View のテストを作成する
View を実装するにあたり、View のテストから作り始めます。
View も in-memory でテストする
Model, ViewModel ともに、in-memory の CoreData でテストしました。
それまでに残っているデータを考慮せずにテストできるので、テストは、in-memory の方がやりやすいです。
Model テスト、ViewModel テスト どちらも API ベースでのテストなので、in-memory の指定が行いやすかったのですが、アプリ経由でのテストになると、引数指定して initializer を呼べません。
アプリの引数指定で、アプリ起動時に、in-memory の指定ができるようにすると便利です。
[TDD] iOSアプリのUITestでのテスト用データの作り方
View(App) の実装 (初期化時に、in-memory を選択できるように)
テストでの実行時に、”TestWithInMemory” と引数に指定することで、InMemory の CoreData を使うようにします。
そのために、App を以下のようにしました。
//
// MyTODOApp.swift
//
// Created by : Tomoaki Yagishita on 2020/12/10
// © 2020 SmallDeskSoftware
//
import SwiftUI
import os
@main
struct MyTODOApp: App {
static let logger = Logger(subsystem: "com.smalldesksoftware.MyTODO.MyTODOApp", category: "MyTODOApp")
@StateObject var viewModel: MyTODOViewModel
init() {
// (1)
let inMemory:Bool = ProcessInfo.processInfo.arguments.contains("TestWithInMemory")
// (2)
_viewModel = StateObject(wrappedValue: MyTODOViewModel(inMemory))
}
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
- アプリ起動時の引数は、ProcessInfo.processInfo.arguments 経由で取得できます
- StateObject をこのように初期化することもできます
こうすることで、起動時の引数指定で、CoreData を In-Memory にすることができます。
View のテストコード
In-Memory で起動できるようになったので、View のテストコードを書いていきます。
まずは、In-Memory で起動して、List が空であり ボタン等が存在することをテストします。
UITest ように新しいファイル を作成しました。(ターゲットは、UITest です)
//
// View_Tests.swift
//
// Created by : Tomoaki Yagishita on 2020/12/12
// © 2020 SmallDeskSoftware
//
import XCTest
class ViewTests_UITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func test_launch_noData_EmptyList() throws {
let app = XCUIApplication()
// (1)
app.launchArguments.append("TestWithInMemory")
app.launch()
// (2)
let todoList = app.tables["TODOList"]
XCTAssertTrue(todoList.exists)
// (3)
let listRows = todoList.cells
XCTAssertEqual(listRows.count, 0)
// (4)
let navBar = app.navigationBars["MyTODO"]
XCTAssertTrue(navBar.exists)
// (5)
let addButton = navBar.buttons.element(boundBy: 1)
XCTAssertTrue(addButton.exists)
let editButton = navBar.buttons.element(boundBy: 0)
XCTAssertTrue(editButton.exists)
}
func test_addAndRemoveItem_afterLaunch_todoItemsShouldBeUpdated() throws {
let app = XCUIApplication()
app.launchArguments.append("TestWithInMemory")
app.launch()
let todoList = app.tables["TODOList"]
let listRows = todoList.cells
XCTAssertEqual(listRows.count, 0)
let navBar = app.navigationBars["MyTODO"]
let addButton = navBar.buttons.element(boundBy: 1)
let editButton = navBar.buttons.element(boundBy: 0)
// (6)
// add
addButton.tap()
XCTAssertEqual(listRows.count, 1)
addButton.tap()
XCTAssertEqual(listRows.count, 2)
// (7)
// remove
editButton.tap()
listRows.element(boundBy: 1).buttons["Delete "].tap()
listRows.element(boundBy: 1).buttons["Delete"].tap()
XCTAssertEqual(listRows.count, 1)
}
}
- TestWithInMemory という引数を指定して、アプリを起動します
- TODOList という Accessibility ID が付与された List を取得します
- List に含まれる 行 を 取得します
- List に含まれる行が 0 であることをテストします
- NavigationBar に Edit ボタンと 追加ボタン があることをテストします
- 追加ボタンを2回押下し、リストが2行になることをテストします
- Edit ボタンを押下し、行に表示される削除ボタンを押して要素を削除、リストの行が1行になっていることをテストします
まだ、View を修正していないため、エラーとなります。
View の実装
まずは、以下の機能を実装します。
- TODO item をリスト表示
- TODO item を追加
- TODO item を削除 (ForEach の .onDelete を利用)
以下のようなコードになります。
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2020/12/10
// © 2020 SmallDeskSoftware
//
import SwiftUI
import CoreData
struct ContentView: View {
@EnvironmentObject var viewModel: MyTODOViewModel
var body: some View {
NavigationView {
List {
// (1)
ForEach(viewModel.todoItems, id:\.self) { item in
Text("\(item.id?.uuidString ?? "noID") \(item.title)")
}
// (2)
.onDelete(perform: deleteItems)
}
// (3)
.accessibility(identifier: "TODOList")
// (3)
.navigationTitle("MyTODO")
// (4)
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarLeading) {
EditButton()
}
#endif
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
// (5)
private func addItem() {
withAnimation {
_ = viewModel.createTODOItem("NewItem", "NewDetail")
}
}
// (6)
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { viewModel.todoItems[$0] }.forEach(viewModel.deleteTODOItem)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(MyTODOViewModel(false))
}
}
- ForEach を使って、TODOItem を表示します。Title と ID を表示しています
- 削除する機能は、SwiftUI の提供する onDelete を使います
- UITest からアクセスできるように Accessibility ID を設定します
- Toolbar に、EditButton と 追加するボタンを設定しています。
- 追加ボタンが押された時に、ViewModel の該当メソッドを呼びます
- 削除するときには、ViewModel の該当メソッドを呼びます
シミュレータで動かしてみると、以下のようになります。
テスト
この View の実装を使うと、先ほど作成したテストをパスすることがわかります。
ここまでの振り返り
これで、非常にシンプルな View の実装はできたことになります。
以下の点を改良します。
- TODOItem 表示の改良(タイトル、詳細を表示)
- TODOItem 追加時に、タイトル等を指定できるようにする
- TODOItem に、完了フラグを追加する
いろいろと考えられると思いますが、まずは上記の点を改良していきます。
一度、コードのリファクタリングをしてから、上記の点を改良していきます。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link