優れた開発者として、あなたはあなたが書いたソフトウェアのすべての機能とすべての可能なコードパスと結果をテストするために最善を尽くします。しかし、考えられるすべての結果と、ユーザーがたどる可能性のあるすべてのパスを手動でテストできることは非常にまれであり、珍しいことです。
アプリケーションが大きく複雑になるにつれて、手動テストで何かを見逃す可能性が大幅に高まります。
UIとバックエンドサービスAPIの両方の自動テストにより、すべてが意図したとおりに機能することを確信でき、開発、リファクタリング、新機能の追加、または既存の機能の変更時のストレスを軽減できます。
自動テストを使用すると、次のことができます。
この記事では、iOSプラットフォームで自動テストを構築して実行する方法について説明します。
ユニットテストとUIテストを区別することが重要です。
に 単体テスト テスト 特定の機能 下で 特定のコンテキスト 。単体テストは、コードのテストされた部分(通常は単一の関数)が想定どおりに機能することを確認します。たくさんの本があり、 ユニットテストに関する記事 、そのため、この投稿では取り上げません。
UIテスト ユーザーインターフェイスをテストするためのものです。たとえば、ビューが意図したとおりに更新されているかどうか、またはユーザーが特定のUI要素を操作したときに特定のアクションがトリガーされるかどうかをテストできます。
各UIテストは 特定のユーザーインタラクション アプリケーションのUIを使用します。自動テストは、単体テストとUIテストの両方のレベルで実行できます。
XCodeは、ユニットとUIのテストをすぐにサポートするため、プロジェクトに追加するのは簡単で簡単です。新しいプロジェクトを作成するときは、「単体テストを含める」と「UIテストを含める」をチェックするだけです。
プロジェクトが作成されると、これら2つのオプションがオンになっていると、2つの新しいターゲットがプロジェクトに追加されます。新しいターゲット名には、名前の末尾に「Tests」または「UITests」が追加されます。
それでおしまい。プロジェクトの自動テストを作成する準備が整いました。
すでに既存のプロジェクトがあり、UIと単体テストのサポートを追加したい場合は、もう少し作業を行う必要がありますが、それも非常に簡単でシンプルです。
に移動 ファイル→新規→ターゲット 選択します iOSユニットテストバンドル ユニットテストまたは iOSUIテストバンドル UIテスト用。
押す 次 。
ターゲットオプション画面では、すべてをそのままにしておくことができます(複数のターゲットがあり、特定のターゲットのみをテストする場合は、[テストするターゲット]ドロップダウンでターゲットを選択します)。
押す 終了 。 UIテストについてこの手順を繰り返すと、既存のプロジェクトで自動テストの作成を開始する準備が整います。
単体テストの作成を開始する前に、それらの構造を理解する必要があります。プロジェクトに単体テストを含めると、サンプルのテストクラスが作成されます。私たちの場合、次のようになります。
import XCTest class TestingIOSTests: XCTestCase { override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } func testPerformanceExample() { // This is an example of a performance test case. self.measure { // Put the code you want to measure the time of here. } } }
理解するための最も重要な方法はsetUp
です。およびtearDown
。 setUp
メソッドが呼び出されます 前 すべてのテスト方法、tearDown
メソッドが呼び出されます 後 すべてのテスト方法。このサンプルテストクラスで定義されたテストを実行すると、メソッドは次のように実行されます。
setUp→testExample→tearDownsetUp→testPerformanceExample→tearDown
ヒント:テストは、cmd + Uを押すか、[製品]→[テスト]を選択するか、オプションメニューが表示されるまで[実行]ボタンをクリックして押したままにして、メニューから[テスト]を選択することで実行されます。
特定のテストメソッドを1つだけ実行する場合は、メソッド名の左側にあるボタンを押します(下の画像を参照)。
これで、テストを作成する準備がすべて整ったら、サンプルクラスといくつかのメソッドを追加してテストできます。
ユーザー登録を担当するクラスを追加します。ユーザーは、電子メールアドレス、パスワード、およびパスワードの確認を入力します。サンプルクラスは、入力を検証し、電子メールアドレスの可用性を確認し、ユーザー登録を試みます。
注意: この例では、 MVVM(またはModel-View-ViewModel) アーキテクチャパターン。
MVVMが使用されるのは、アプリケーションのアーキテクチャがよりクリーンでテストしやすくなるためです。
MVVMを使用すると、ビジネスロジックをプレゼンテーションロジックから簡単に分離できるため、ViewControllerの大規模な問題を回避できます。
MVVMアーキテクチャの詳細はこの記事の範囲外ですが、詳細については、 この記事 。
ユーザー登録を担当するビューモデルクラスを作成しましょう。 。
オラクルデータベース設計のベストプラクティス
class RegisterationViewModel { var emailAddress: String? { didSet { enableRegistrationAttempt() } } var password: String? { didSet { enableRegistrationAttempt() } } var passwordConfirmation: String? { didSet { enableRegistrationAttempt() } } var registrationEnabled = Dynamic(false) var errorMessage = Dynamic('') var loginSuccessful = Dynamic(false) var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } }
まず、いくつかのプロパティ、動的プロパティ、およびinitメソッドを追加しました。
Dynamic
について心配する必要はありませんタイプ。これはMVVMアーキテクチャの一部です。
Dynamic
の場合値はtrueに設定され、ビューコントローラーはRegistrationViewModel
にバインド(接続)されます登録ボタンが有効になります。 loginSuccessful
の場合trueに設定すると、接続されたビューが自動的に更新されます。
次に、パスワードと電子メール形式の有効性を確認するためのいくつかの方法を追加しましょう。
func enableRegistrationAttempt() { registrationEnabled.value = emailValid() && passwordValid() } func emailValid() -> Bool { let emailRegEx = '[A-Z0-9a-z._%+-] [email protected] [A-Za-z0-9.-]+\.[A-Za-z]{2,}' let emailTest = NSPredicate(format:'SELF MATCHES %@', emailRegEx) return emailTest.evaluate(with: emailAddress) } func passwordValid() -> Bool { guard let password = password, let passwordConfirmation = passwordConfirmation else { return false } let isValid = (password == passwordConfirmation) && password.characters.count >= 6 return isValid }
ユーザーがメールまたはパスワードフィールドに何かを入力するたびに、enableRegistrationAttempt
メソッドは、電子メールとパスワードが正しい形式であるかどうかを確認し、registrationEnabled
を介して登録ボタンを有効または無効にします動的プロパティ。
例を単純にするために、2つの簡単な方法を追加します。1つは電子メールの可用性を確認する方法、もう1つは指定されたユーザー名とパスワードで登録を試みる方法です。
func checkEmailAvailability(email: String, withCallback callback: @escaping (Bool?)->(Void)) { networkService.checkEmailAvailability(email: email) { (available, error) in if let _ = error { self.errorMessage.value = 'Our custom error message' } else if !available { self.errorMessage.value = 'Sorry, provided email address is already taken' self.registrationEnabled.value = false callback(available) } } } func attemptUserRegistration() { guard registrationEnabled.value == true else { return } // To keep the example as simple as possible, password won't be hashed guard let emailAddress = emailAddress, let passwordHash = password else { return } networkService.attemptRegistration(forUserEmail: emailAddress, withPasswordHash: passwordHash) { (success, error) in // Handle the response if let _ = error { self.errorMessage.value = 'Our custom error message' } else { self.loginSuccessful.value = true } } }
これらの2つの方法は、NetworkServiceを使用して、電子メールが利用可能かどうかを確認し、登録を試みます。
この例を単純にするために、NetworkService実装はバックエンドAPIを使用していませんが、結果を偽造する単なるスタブです。 NetworkServiceは、プロトコルとその実装クラスとして実装されます。
typealias RegistrationAttemptCallback = (_ success: Bool, _ error: NSError?) -> Void typealias EmailAvailabilityCallback = (_ available: Bool, _ error: NSError?) -> Void protocol NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) }
NetworkServiceは、登録試行と電子メールの可用性チェックメソッドの2つのメソッドのみを含む非常に単純なプロトコルです。プロトコルの実装はNetworkServiceImplクラスです。
class NetworkServiceImpl: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } }
どちらのメソッドも、(ネットワーク要求の時間遅延を偽って)しばらく待ってから、適切なコールバックメソッドを呼び出すだけです。
ヒント:プロトコル(他のプログラミング言語ではインターフェースとも呼ばれます)を使用することをお勧めします。 「インターフェースへのプログラミングの原則」を検索すると、詳細を読むことができます。また、単体テストでどのように機能するかについても説明します。
これで、例を設定すると、このクラスのメソッドをカバーする単体テストを作成できます。
Webサイトの例のヒューリスティック評価
ビューモデルの新しいテストクラスを作成します。 TestingIOSTests
を右クリックしますプロジェクトナビゲータペインのフォルダで、[新しいファイル]→[単体テストケースクラス]を選択し、RegistrationViewModelTests
という名前を付けます。
testExample
を削除しますおよびtestPerformanceExample
独自のテストメソッドを作成したいので、メソッド。
Swiftはモジュールを使用し、テストはアプリケーションのコードとは異なるモジュールにあるため、アプリケーションのモジュールを@testable
としてインポートする必要があります。 importステートメントとクラス定義の下に@testable import TestingIOS
を追加します(またはアプリケーションのモジュール名)。これがないと、アプリケーションのクラスやメソッドを参照できません。
registrationViewModel
を追加します変数。
空のテストクラスは次のようになります。
import XCTest @testable import TestingIOS class RegistrationViewModelTests: XCTestCase { var registrationViewModel: RegisterationViewModel? override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } }
emailValid
のテストを書いてみましょう方法。 testEmailValid
という新しいテストメソッドを作成します。 test
を追加することが重要です名前の先頭にあるキーワード。そうしないと、メソッドはテストメソッドとして認識されません。
テスト方法は次のようになります。
func testEmailValid() { let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl()) registrationVM.emailAddress = 'email.test.com' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = ' [email protected] ' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = nil XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = ' [email protected] ' XCTAssert(registrationVM.emailValid(), '(registrationVM.emailAddress) should be correct') }
このテストメソッドは、アサーションメソッドXCTAssert
を使用します。この場合、条件がtrueかfalseかをチェックします。
条件がfalseの場合、assertは(テストとともに)失敗し、メッセージが書き出されます。
テストで使用できるassertメソッドはたくさんあります。各assertメソッドを説明して表示すると、簡単に独自の記事を作成できるため、ここでは詳しく説明しません。
使用可能なアサートメソッドの例は次のとおりです。XCTAssertEqualObjects
、XCTAssertGreaterThan
、XCTAssertNil
、XCTAssertTrue
またはXCTAssertThrows
。
利用可能なassertメソッドの詳細を読むことができます ここに 。
ここでテストを実行すると、テストメソッドは合格します。最初のテストメソッドを正常に作成しましたが、まだプライムタイムの準備が整っていません。このテスト方法には、以下に詳述するように、まだ3つの問題(1つは大きな問題と2つの小さな問題)があります。
単体テストのコア原則の1つは、すべてのテストが外部の要因や依存関係から独立している必要があるということです。ユニットテストはアトミックである必要があります。
ある時点でサーバーからAPIメソッドを呼び出すメソッドをテストしている場合、テストはネットワークコードとサーバーの可用性に依存します。テスト時にサーバーが機能していない場合、テストは失敗するため、テストしたメソッドが機能していないと誤って非難します。
この場合、RegistrationViewModel
のメソッドをテストしています。
RegistrationViewModel
NetworkServiceImpl
に依存しますテストしたメソッドemailValid
がNetworkServiceImpl
に依存していないことがわかっていてもクラス直接。
単体テストを作成するときは、外部の依存関係をすべて削除する必要があります。ただし、RegistrationViewModel
の実装を変更せずにNetworkServiceの依存関係を削除するにはどうすればよいですか。クラス?
この問題には簡単な解決策があり、それは オブジェクトモック 。 RegistrationViewModel
をよく見ると、実際にはNetworkService
に依存していることがわかります。プロトコル。
class RegisterationViewModel { … // It depends on NetworkService. RegistrationViewModel doesn't even care if NetworkServiceImple exists var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } ...
RegistrationViewModel
のとき初期化中です、NetworkService
の実装プロトコルがRegistrationViewModel
に与えられる(または注入される)オブジェクト。
この原理は コンストラクターによる依存性注入 (( より多くの種類の依存性注入があります )。
オンラインでの依存性注入に関する興味深い記事がたくさんあります。 objc.io 。
依存性注入を簡単でわかりやすい方法で説明する短いが興味深い記事もあります ここに 。
さらに、単一責任の原則とDIに関する優れた記事が ApeeScapeブログ 。
RegistrationViewModel
のときがインスタンス化されると、コンストラクターにNetworkServiceプロトコル実装が注入されます(したがって、依存性注入の原則の名前が付けられています)。
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
ビューモデルクラスはプロトコルにのみ依存するため、カスタム(またはモック)の作成を妨げるものは何もありませんNetworkService
実装クラスとモッククラスをビューモデルオブジェクトに挿入します。
モックを作成しましょうNetworkService
プロトコルの実装。
TestingIOSTests
を右クリックして、新しいSwiftファイルをテストターゲットに追加します。 Project Navigatorのフォルダで、「New File」を選択し、「Swift file」を選択して、NetworkServiceMock
という名前を付けます。
これは、モックされたクラスがどのように見えるかです。
import Foundation @testable import TestingIOS class NetworkServiceMock: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(false, nil) }) } }
この時点では、実際の実装(NetworkServiceImpl
)と大差ありませんが、実際の状況では、実際のNetworkServiceImpl
ネットワークコード、応答処理、および同様の機能があります。
私たちのモッククラスは何もしません。これがモッククラスのポイントです。それが何もしない場合は、テストに干渉しません。
テストの最初の問題を修正するために、次のように置き換えてテストメソッドを更新しましょう。
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
と:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceMock())
setUp
がありますおよびtearDown
理由のための方法。
これらのメソッドは、テストに必要なすべての必要なオブジェクトを初期化またはセットアップするために使用されます。これらのメソッドを使用して、すべてのテストメソッドで同じinitメソッドまたはsetupメソッドを記述して、コードの重複を回避する必要があります。特に特定のテストメソッドに対して本当に特定の構成がある場合は、setupメソッドとtearDownメソッドを使用しないことは必ずしも大きな問題ではありません。
RegistrationViewModel
の初期化以降クラスは非常に単純です。セットアップメソッドとtearDownメソッドを使用するようにテストクラスをリファクタリングします。
RegistrationViewModelTests
次のようになります。
class RegistrationViewModelTests: XCTestCase { var registrationVM: RegisterationViewModel! override func setUp() { super.setUp() registrationVM = RegisterationViewModel(networkService: NetworkServiceMock()) } override func tearDown() { registrationVM = nil super.tearDown() } func testEmailValid() { registrationVM.emailAddress = 'email.test.com' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') ... } }
これは大きな問題ではありませんが、メソッドごとに1つのアサーションを持つことを提唱する人もいます。
この原則の主な理由は、エラー検出です。
1つのテストメソッドに複数のアサーションがあり、最初のアサーションが失敗した場合、テストメソッド全体が失敗としてマークされます。他のアサーションはテストされません。
このようにして、一度に1つのエラーのみを検出します。他のアサートが失敗するか成功するかはわかりません。
一度に修正できるエラーは1つだけなので、1つのメソッドに複数のアサーションがあることは必ずしも悪いことではありません。したがって、一度に1つのエラーを検出することはそれほど大きな問題ではない可能性があります。
私たちの場合、電子メール形式の有効性がテストされます。これは1つの関数にすぎないため、テストを読みやすく理解しやすくするために、すべてのアサートを1つのメソッドにグループ化する方が論理的かもしれません。
この問題は実際には大きな問題ではなく、まったく問題ではないと主張する人もいるため、テスト方法はそのままにしておきます。
独自の単体テストを作成するときは、各テスト方法でどのパスを使用するかを決定するのはあなた次第です。ほとんどの場合、テスト哲学ごとに1つの主張が理にかなっている場所と、そうでない場所があることがわかります。
アプリケーションがどれほど単純であっても、特にUIを独自のスレッドで実行したい場合は特に、別のスレッドで非同期に実行する必要があるメソッドが存在する可能性が高くなります。
単体テストと非同期呼び出しの主な問題は、非同期呼び出しが終了するまでに時間がかかることですが、単体テストは終了するまで待機しません。単体テストは非同期ブロック内のコードが実行される前に終了するため、テストは常に同じ結果で終了します(非同期ブロックに何を書き込んでも)。
これを実証するために、checkEmailAvailability
のテストを作成しましょう。方法。
func testCheckEmailAvailability() { registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: ' [email protected] ') { available in XCTAssert(self.registrationVM.registrationEnabled.value == false, 'Email address is not available, registration should be disabled') } }
ここでは、電子メールが利用できない(別のユーザーがすでに取得している)ことをメソッドが通知した後、registrationEnabled変数がfalseに設定されるかどうかをテストします。
このテストを実行すると、合格します。しかし、もう1つ試してみてください。アサーションを次のように変更します。
XCTAssert(self.registrationVM.registrationEnabled.value == true, 'Email address is not available, registration should be disabled')
テストを再度実行すると、再度合格します。
これは、アサートがアサートされていないためです。ユニットテストは、コールバックブロックが実行される前に終了しました(モックされたネットワークサービスの実装では、戻る前に1秒間待機するように設定されていることに注意してください)。
幸い、Xcode 6では、AppleはXCTestフレームワークにテストの期待値をXCTestExpectation
として追加しました。クラス。 XCTestExpectation
クラスは次のように機能します。
waitForExpectationWithTimer
を設定する必要がありますブロック。期待が満たされたとき、またはタイマーが切れたときのいずれか早い方で実行されます。XCTestExpectation
を使用するようにテストを書き直してみましょうクラス。
func testCheckEmailAvailability() { // 1. Setting the expectation let exp = expectation(description: 'Check email availability') registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: ' [email protected] ') { available in XCTAssert(self.registrationVM.registrationEnabled.value == true, 'Email address is not available, registration should be disabled') // 2. Fulfilling the expectation exp.fulfill() } // 3. Waiting for expectation to fulfill waitForExpectations(timeout: 3.0) { error in if let _ = error { XCTAssert(false, 'Timeout while checking email availability') } } }
ここでテストを実行すると、失敗します-当然のことです。テストに合格するように修正しましょう。アサーションを次のように変更します。
XCTAssert(self.registrationVM.registrationEnabled.value == false, 'Email address is not available, registration should be disabled')
テストを再度実行して、合格することを確認します。ネットワークサービスのモック実装で遅延時間を変更して、期待タイマーが切れた場合に何が起こるかを確認できます。
無制限のお金で偽のクレジットカード2017
サンプルプロジェクトメソッドattemptUserRegistration
NetworkService.attemptRegistration
を使用します非同期で実行されるコードを含むメソッド。このメソッドは、ユーザーをバックエンドサービスに登録しようとします。
このデモアプリケーションでは、メソッドは1秒間待機してネットワーク呼び出しをシミュレートし、登録の成功を偽造します。登録が成功した場合loginSuccessful
値はtrueに設定されます。この動作を検証するために単体テストを作成しましょう。
func testAttemptRegistration() { registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.attemptUserRegistration() XCTAssert(registrationVM.loginSuccessful.value, 'Login must be successful') }
実行した場合、loginSuccessful
が原因で、このテストは失敗します。非同期networkService.attemptRegistration
まで値はtrueに設定されませんメソッドが終了しました。
モックを作成したのでNetworkServiceImpl
ここでattemptRegistration
メソッドは、正常な登録を返す前に1秒間待機します。GrandCentralDispatch(GCD)を使用するだけで、asyncAfter
を利用できます。 1秒後にアサートをチェックするメソッド。 GCDを追加した後asyncAfter
テストコードは次のようになります。
func testAttemptRegistration() { registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.passwordConfirmation = '123456' registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, 'Login must be successful') } }
注意を払っていれば、テストメソッドが実行されるため、これはまだ機能しないことがわかります 前 asyncAfter
ブロックが実行され、メソッドは 常に 結果として正常に合格します。幸い、XCTestException
がありますクラス。
XCTestException
を使用するようにメソッドを書き直してみましょうクラス:
func testAttemptRegistration() { let exp = expectation(description: 'Check registration attempt') registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.passwordConfirmation = '123456' registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, 'Login must be successful') exp.fulfill() } waitForExpectations(timeout: 4.0) { error in if let _ = error { XCTAssert(false, 'Timeout while attempting a registration') } } }
RegistrationViewModel
をカバーする単体テストにより、新しい機能を追加したり、既存の機能を更新したりしても、何も壊れないという確信が持てるようになりました。
重要な注意点: ユニットテストは、対象となるメソッドの機能が変更されたときに更新されない場合、その価値を失います。単体テストの作成は、アプリケーションの残りの部分についていく必要があるプロセスです。
ヒント:ライティングテストを最後まで延期しないでください。開発中にテストを作成します。このようにして、何をテストする必要があるのか、そして何が国境のケースであるのかをよりよく理解することができます。
すべての単体テストが完全に開発され、正常に実行された後、コードの各ユニットが正しく機能していることを確信できますが、それはアプリケーション全体が意図したとおりに機能していることを意味しますか?
そこで登場するのが統合テストであり、その中でUIテストは不可欠なコンポーネントです。
UIテストを開始する前に、テストするUI要素とインタラクション(またはユーザーストーリー)がいくつかある必要があります。簡単なビューとそのビューコントローラを作成しましょう。
Main.storyboard
を開きます下の画像のような単純なViewControllerを作成します。
メールテキストフィールドタグを100、パスワードテキストフィールドタグを101、パスワード確認タグを102に設定します。
RegistrationViewController.swift
すべてのコンセントをストーリーボードに接続します。import UIKit class RegistrationViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var passwordConfirmationTextField: UITextField! @IBOutlet weak var registerButton: UIButton! private struct TextFieldTags { static let emailTextField = 100 static let passwordTextField = 101 static let confirmPasswordTextField = 102 } var viewModel: RegisterationViewModel? override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self passwordTextField.delegate = self passwordConfirmationTextField.delegate = self bindViewModel() } }
ここに追加していますIBOutlets
およびTextFieldTags
クラスへの構造体。
これにより、編集中のテキストフィールドを特定できるようになります。ビューモデルの動的プロパティを利用するには、ビューコントローラで動的プロパティを「バインド」する必要があります。 bindViewModel
でそれを行うことができます方法:
fileprivate func bindViewModel() { if let viewModel = viewModel { viewModel.registrationEnabled.bindAndFire { self.registerButton.isEnabled = iOS用の自動テストを作成する方法
優れた開発者として、あなたはあなたが書いたソフトウェアのすべての機能とすべての可能なコードパスと結果をテストするために最善を尽くします。しかし、考えられるすべての結果と、ユーザーがたどる可能性のあるすべてのパスを手動でテストできることは非常にまれであり、珍しいことです。
アプリケーションが大きく複雑になるにつれて、手動テストで何かを見逃す可能性が大幅に高まります。
UIとバックエンドサービスAPIの両方の自動テストにより、すべてが意図したとおりに機能することを確信でき、開発、リファクタリング、新機能の追加、または既存の機能の変更時のストレスを軽減できます。
自動テストを使用すると、次のことができます。
この記事では、iOSプラットフォームで自動テストを構築して実行する方法について説明します。
ユニットテストとUIテストを区別することが重要です。
に 単体テスト テスト 特定の機能 下で 特定のコンテキスト 。単体テストは、コードのテストされた部分(通常は単一の関数)が想定どおりに機能することを確認します。たくさんの本があり、 ユニットテストに関する記事 、そのため、この投稿では取り上げません。
UIテスト ユーザーインターフェイスをテストするためのものです。たとえば、ビューが意図したとおりに更新されているかどうか、またはユーザーが特定のUI要素を操作したときに特定のアクションがトリガーされるかどうかをテストできます。
各UIテストは 特定のユーザーインタラクション アプリケーションのUIを使用します。自動テストは、単体テストとUIテストの両方のレベルで実行できます。
XCodeは、ユニットとUIのテストをすぐにサポートするため、プロジェクトに追加するのは簡単で簡単です。新しいプロジェクトを作成するときは、「単体テストを含める」と「UIテストを含める」をチェックするだけです。
プロジェクトが作成されると、これら2つのオプションがオンになっていると、2つの新しいターゲットがプロジェクトに追加されます。新しいターゲット名には、名前の末尾に「Tests」または「UITests」が追加されます。
それでおしまい。プロジェクトの自動テストを作成する準備が整いました。
すでに既存のプロジェクトがあり、UIと単体テストのサポートを追加したい場合は、もう少し作業を行う必要がありますが、それも非常に簡単でシンプルです。
に移動 ファイル→新規→ターゲット 選択します iOSユニットテストバンドル ユニットテストまたは iOSUIテストバンドル UIテスト用。
押す 次 。
ターゲットオプション画面では、すべてをそのままにしておくことができます(複数のターゲットがあり、特定のターゲットのみをテストする場合は、[テストするターゲット]ドロップダウンでターゲットを選択します)。
押す 終了 。 UIテストについてこの手順を繰り返すと、既存のプロジェクトで自動テストの作成を開始する準備が整います。
単体テストの作成を開始する前に、それらの構造を理解する必要があります。プロジェクトに単体テストを含めると、サンプルのテストクラスが作成されます。私たちの場合、次のようになります。
import XCTest class TestingIOSTests: XCTestCase { override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() } func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } func testPerformanceExample() { // This is an example of a performance test case. self.measure { // Put the code you want to measure the time of here. } } }
理解するための最も重要な方法はsetUp
です。およびtearDown
。 setUp
メソッドが呼び出されます 前 すべてのテスト方法、tearDown
メソッドが呼び出されます 後 すべてのテスト方法。このサンプルテストクラスで定義されたテストを実行すると、メソッドは次のように実行されます。
setUp→testExample→tearDownsetUp→testPerformanceExample→tearDown
ヒント:テストは、cmd + Uを押すか、[製品]→[テスト]を選択するか、オプションメニューが表示されるまで[実行]ボタンをクリックして押したままにして、メニューから[テスト]を選択することで実行されます。
特定のテストメソッドを1つだけ実行する場合は、メソッド名の左側にあるボタンを押します(下の画像を参照)。
これで、テストを作成する準備がすべて整ったら、サンプルクラスといくつかのメソッドを追加してテストできます。
ユーザー登録を担当するクラスを追加します。ユーザーは、電子メールアドレス、パスワード、およびパスワードの確認を入力します。サンプルクラスは、入力を検証し、電子メールアドレスの可用性を確認し、ユーザー登録を試みます。
注意: この例では、 MVVM(またはModel-View-ViewModel) アーキテクチャパターン。
MVVMが使用されるのは、アプリケーションのアーキテクチャがよりクリーンでテストしやすくなるためです。
MVVMを使用すると、ビジネスロジックをプレゼンテーションロジックから簡単に分離できるため、ViewControllerの大規模な問題を回避できます。
MVVMアーキテクチャの詳細はこの記事の範囲外ですが、詳細については、 この記事 。
ユーザー登録を担当するビューモデルクラスを作成しましょう。 。
class RegisterationViewModel { var emailAddress: String? { didSet { enableRegistrationAttempt() } } var password: String? { didSet { enableRegistrationAttempt() } } var passwordConfirmation: String? { didSet { enableRegistrationAttempt() } } var registrationEnabled = Dynamic(false) var errorMessage = Dynamic('') var loginSuccessful = Dynamic(false) var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } }
まず、いくつかのプロパティ、動的プロパティ、およびinitメソッドを追加しました。
Dynamic
について心配する必要はありませんタイプ。これはMVVMアーキテクチャの一部です。
Dynamic
の場合値はtrueに設定され、ビューコントローラーはRegistrationViewModel
にバインド(接続)されます登録ボタンが有効になります。 loginSuccessful
の場合trueに設定すると、接続されたビューが自動的に更新されます。
次に、パスワードと電子メール形式の有効性を確認するためのいくつかの方法を追加しましょう。
func enableRegistrationAttempt() { registrationEnabled.value = emailValid() && passwordValid() } func emailValid() -> Bool { let emailRegEx = '[A-Z0-9a-z._%+-] [email protected] [A-Za-z0-9.-]+\.[A-Za-z]{2,}' let emailTest = NSPredicate(format:'SELF MATCHES %@', emailRegEx) return emailTest.evaluate(with: emailAddress) } func passwordValid() -> Bool { guard let password = password, let passwordConfirmation = passwordConfirmation else { return false } let isValid = (password == passwordConfirmation) && password.characters.count >= 6 return isValid }
ユーザーがメールまたはパスワードフィールドに何かを入力するたびに、enableRegistrationAttempt
メソッドは、電子メールとパスワードが正しい形式であるかどうかを確認し、registrationEnabled
を介して登録ボタンを有効または無効にします動的プロパティ。
例を単純にするために、2つの簡単な方法を追加します。1つは電子メールの可用性を確認する方法、もう1つは指定されたユーザー名とパスワードで登録を試みる方法です。
func checkEmailAvailability(email: String, withCallback callback: @escaping (Bool?)->(Void)) { networkService.checkEmailAvailability(email: email) { (available, error) in if let _ = error { self.errorMessage.value = 'Our custom error message' } else if !available { self.errorMessage.value = 'Sorry, provided email address is already taken' self.registrationEnabled.value = false callback(available) } } } func attemptUserRegistration() { guard registrationEnabled.value == true else { return } // To keep the example as simple as possible, password won't be hashed guard let emailAddress = emailAddress, let passwordHash = password else { return } networkService.attemptRegistration(forUserEmail: emailAddress, withPasswordHash: passwordHash) { (success, error) in // Handle the response if let _ = error { self.errorMessage.value = 'Our custom error message' } else { self.loginSuccessful.value = true } } }
これらの2つの方法は、NetworkServiceを使用して、電子メールが利用可能かどうかを確認し、登録を試みます。
この例を単純にするために、NetworkService実装はバックエンドAPIを使用していませんが、結果を偽造する単なるスタブです。 NetworkServiceは、プロトコルとその実装クラスとして実装されます。
typealias RegistrationAttemptCallback = (_ success: Bool, _ error: NSError?) -> Void typealias EmailAvailabilityCallback = (_ available: Bool, _ error: NSError?) -> Void protocol NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) }
NetworkServiceは、登録試行と電子メールの可用性チェックメソッドの2つのメソッドのみを含む非常に単純なプロトコルです。プロトコルの実装はNetworkServiceImplクラスです。
class NetworkServiceImpl: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } }
どちらのメソッドも、(ネットワーク要求の時間遅延を偽って)しばらく待ってから、適切なコールバックメソッドを呼び出すだけです。
ヒント:プロトコル(他のプログラミング言語ではインターフェースとも呼ばれます)を使用することをお勧めします。 「インターフェースへのプログラミングの原則」を検索すると、詳細を読むことができます。また、単体テストでどのように機能するかについても説明します。
これで、例を設定すると、このクラスのメソッドをカバーする単体テストを作成できます。
ビューモデルの新しいテストクラスを作成します。 TestingIOSTests
を右クリックしますプロジェクトナビゲータペインのフォルダで、[新しいファイル]→[単体テストケースクラス]を選択し、RegistrationViewModelTests
という名前を付けます。
testExample
を削除しますおよびtestPerformanceExample
独自のテストメソッドを作成したいので、メソッド。
Swiftはモジュールを使用し、テストはアプリケーションのコードとは異なるモジュールにあるため、アプリケーションのモジュールを@testable
としてインポートする必要があります。 importステートメントとクラス定義の下に@testable import TestingIOS
を追加します(またはアプリケーションのモジュール名)。これがないと、アプリケーションのクラスやメソッドを参照できません。
registrationViewModel
を追加します変数。
空のテストクラスは次のようになります。
import XCTest @testable import TestingIOS class RegistrationViewModelTests: XCTestCase { var registrationViewModel: RegisterationViewModel? override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } }
emailValid
のテストを書いてみましょう方法。 testEmailValid
という新しいテストメソッドを作成します。 test
を追加することが重要です名前の先頭にあるキーワード。そうしないと、メソッドはテストメソッドとして認識されません。
テスト方法は次のようになります。
func testEmailValid() { let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl()) registrationVM.emailAddress = 'email.test.com' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = ' [email protected] ' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = nil XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') registrationVM.emailAddress = ' [email protected] ' XCTAssert(registrationVM.emailValid(), '(registrationVM.emailAddress) should be correct') }
このテストメソッドは、アサーションメソッドXCTAssert
を使用します。この場合、条件がtrueかfalseかをチェックします。
条件がfalseの場合、assertは(テストとともに)失敗し、メッセージが書き出されます。
テストで使用できるassertメソッドはたくさんあります。各assertメソッドを説明して表示すると、簡単に独自の記事を作成できるため、ここでは詳しく説明しません。
使用可能なアサートメソッドの例は次のとおりです。XCTAssertEqualObjects
、XCTAssertGreaterThan
、XCTAssertNil
、XCTAssertTrue
またはXCTAssertThrows
。
利用可能なassertメソッドの詳細を読むことができます ここに 。
ここでテストを実行すると、テストメソッドは合格します。最初のテストメソッドを正常に作成しましたが、まだプライムタイムの準備が整っていません。このテスト方法には、以下に詳述するように、まだ3つの問題(1つは大きな問題と2つの小さな問題)があります。
単体テストのコア原則の1つは、すべてのテストが外部の要因や依存関係から独立している必要があるということです。ユニットテストはアトミックである必要があります。
ある時点でサーバーからAPIメソッドを呼び出すメソッドをテストしている場合、テストはネットワークコードとサーバーの可用性に依存します。テスト時にサーバーが機能していない場合、テストは失敗するため、テストしたメソッドが機能していないと誤って非難します。
この場合、RegistrationViewModel
のメソッドをテストしています。
RegistrationViewModel
NetworkServiceImpl
に依存しますテストしたメソッドemailValid
がNetworkServiceImpl
に依存していないことがわかっていてもクラス直接。
単体テストを作成するときは、外部の依存関係をすべて削除する必要があります。ただし、RegistrationViewModel
の実装を変更せずにNetworkServiceの依存関係を削除するにはどうすればよいですか。クラス?
この問題には簡単な解決策があり、それは オブジェクトモック 。 RegistrationViewModel
をよく見ると、実際にはNetworkService
に依存していることがわかります。プロトコル。
class RegisterationViewModel { … // It depends on NetworkService. RegistrationViewModel doesn't even care if NetworkServiceImple exists var networkService: NetworkService init(networkService: NetworkService) { self.networkService = networkService } ...
RegistrationViewModel
のとき初期化中です、NetworkService
の実装プロトコルがRegistrationViewModel
に与えられる(または注入される)オブジェクト。
この原理は コンストラクターによる依存性注入 (( より多くの種類の依存性注入があります )。
オンラインでの依存性注入に関する興味深い記事がたくさんあります。 objc.io 。
依存性注入を簡単でわかりやすい方法で説明する短いが興味深い記事もあります ここに 。
さらに、単一責任の原則とDIに関する優れた記事が ApeeScapeブログ 。
RegistrationViewModel
のときがインスタンス化されると、コンストラクターにNetworkServiceプロトコル実装が注入されます(したがって、依存性注入の原則の名前が付けられています)。
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
ビューモデルクラスはプロトコルにのみ依存するため、カスタム(またはモック)の作成を妨げるものは何もありませんNetworkService
実装クラスとモッククラスをビューモデルオブジェクトに挿入します。
モックを作成しましょうNetworkService
プロトコルの実装。
TestingIOSTests
を右クリックして、新しいSwiftファイルをテストターゲットに追加します。 Project Navigatorのフォルダで、「New File」を選択し、「Swift file」を選択して、NetworkServiceMock
という名前を付けます。
これは、モックされたクラスがどのように見えるかです。
import Foundation @testable import TestingIOS class NetworkServiceMock: NetworkService { func attemptRegistration(forUserEmail email: String, withPasswordHash passwordHash: String, andCallback callback: @escaping RegistrationAttemptCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(true, nil) }) } func checkEmailAvailability(email: String, withCallback callback: @escaping EmailAvailabilityCallback) { // Make it look like method needs some time to communicate with the server DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1), execute: { callback(false, nil) }) } }
この時点では、実際の実装(NetworkServiceImpl
)と大差ありませんが、実際の状況では、実際のNetworkServiceImpl
ネットワークコード、応答処理、および同様の機能があります。
私たちのモッククラスは何もしません。これがモッククラスのポイントです。それが何もしない場合は、テストに干渉しません。
テストの最初の問題を修正するために、次のように置き換えてテストメソッドを更新しましょう。
let registrationVM = RegisterationViewModel(networkService: NetworkServiceImpl())
と:
let registrationVM = RegisterationViewModel(networkService: NetworkServiceMock())
setUp
がありますおよびtearDown
理由のための方法。
これらのメソッドは、テストに必要なすべての必要なオブジェクトを初期化またはセットアップするために使用されます。これらのメソッドを使用して、すべてのテストメソッドで同じinitメソッドまたはsetupメソッドを記述して、コードの重複を回避する必要があります。特に特定のテストメソッドに対して本当に特定の構成がある場合は、setupメソッドとtearDownメソッドを使用しないことは必ずしも大きな問題ではありません。
RegistrationViewModel
の初期化以降クラスは非常に単純です。セットアップメソッドとtearDownメソッドを使用するようにテストクラスをリファクタリングします。
RegistrationViewModelTests
次のようになります。
class RegistrationViewModelTests: XCTestCase { var registrationVM: RegisterationViewModel! override func setUp() { super.setUp() registrationVM = RegisterationViewModel(networkService: NetworkServiceMock()) } override func tearDown() { registrationVM = nil super.tearDown() } func testEmailValid() { registrationVM.emailAddress = 'email.test.com' XCTAssertFalse(registrationVM.emailValid(), '(registrationVM.emailAddress) shouldn't be correct') ... } }
これは大きな問題ではありませんが、メソッドごとに1つのアサーションを持つことを提唱する人もいます。
この原則の主な理由は、エラー検出です。
1つのテストメソッドに複数のアサーションがあり、最初のアサーションが失敗した場合、テストメソッド全体が失敗としてマークされます。他のアサーションはテストされません。
このようにして、一度に1つのエラーのみを検出します。他のアサートが失敗するか成功するかはわかりません。
一度に修正できるエラーは1つだけなので、1つのメソッドに複数のアサーションがあることは必ずしも悪いことではありません。したがって、一度に1つのエラーを検出することはそれほど大きな問題ではない可能性があります。
私たちの場合、電子メール形式の有効性がテストされます。これは1つの関数にすぎないため、テストを読みやすく理解しやすくするために、すべてのアサートを1つのメソッドにグループ化する方が論理的かもしれません。
この問題は実際には大きな問題ではなく、まったく問題ではないと主張する人もいるため、テスト方法はそのままにしておきます。
独自の単体テストを作成するときは、各テスト方法でどのパスを使用するかを決定するのはあなた次第です。ほとんどの場合、テスト哲学ごとに1つの主張が理にかなっている場所と、そうでない場所があることがわかります。
アプリケーションがどれほど単純であっても、特にUIを独自のスレッドで実行したい場合は特に、別のスレッドで非同期に実行する必要があるメソッドが存在する可能性が高くなります。
単体テストと非同期呼び出しの主な問題は、非同期呼び出しが終了するまでに時間がかかることですが、単体テストは終了するまで待機しません。単体テストは非同期ブロック内のコードが実行される前に終了するため、テストは常に同じ結果で終了します(非同期ブロックに何を書き込んでも)。
これを実証するために、checkEmailAvailability
のテストを作成しましょう。方法。
func testCheckEmailAvailability() { registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: ' [email protected] ') { available in XCTAssert(self.registrationVM.registrationEnabled.value == false, 'Email address is not available, registration should be disabled') } }
ここでは、電子メールが利用できない(別のユーザーがすでに取得している)ことをメソッドが通知した後、registrationEnabled変数がfalseに設定されるかどうかをテストします。
このテストを実行すると、合格します。しかし、もう1つ試してみてください。アサーションを次のように変更します。
XCTAssert(self.registrationVM.registrationEnabled.value == true, 'Email address is not available, registration should be disabled')
テストを再度実行すると、再度合格します。
これは、アサートがアサートされていないためです。ユニットテストは、コールバックブロックが実行される前に終了しました(モックされたネットワークサービスの実装では、戻る前に1秒間待機するように設定されていることに注意してください)。
幸い、Xcode 6では、AppleはXCTestフレームワークにテストの期待値をXCTestExpectation
として追加しました。クラス。 XCTestExpectation
クラスは次のように機能します。
waitForExpectationWithTimer
を設定する必要がありますブロック。期待が満たされたとき、またはタイマーが切れたときのいずれか早い方で実行されます。XCTestExpectation
を使用するようにテストを書き直してみましょうクラス。
func testCheckEmailAvailability() { // 1. Setting the expectation let exp = expectation(description: 'Check email availability') registrationVM.registrationEnabled.value = true registrationVM.checkEmailAvailability(email: ' [email protected] ') { available in XCTAssert(self.registrationVM.registrationEnabled.value == true, 'Email address is not available, registration should be disabled') // 2. Fulfilling the expectation exp.fulfill() } // 3. Waiting for expectation to fulfill waitForExpectations(timeout: 3.0) { error in if let _ = error { XCTAssert(false, 'Timeout while checking email availability') } } }
ここでテストを実行すると、失敗します-当然のことです。テストに合格するように修正しましょう。アサーションを次のように変更します。
XCTAssert(self.registrationVM.registrationEnabled.value == false, 'Email address is not available, registration should be disabled')
テストを再度実行して、合格することを確認します。ネットワークサービスのモック実装で遅延時間を変更して、期待タイマーが切れた場合に何が起こるかを確認できます。
サンプルプロジェクトメソッドattemptUserRegistration
NetworkService.attemptRegistration
を使用します非同期で実行されるコードを含むメソッド。このメソッドは、ユーザーをバックエンドサービスに登録しようとします。
このデモアプリケーションでは、メソッドは1秒間待機してネットワーク呼び出しをシミュレートし、登録の成功を偽造します。登録が成功した場合loginSuccessful
値はtrueに設定されます。この動作を検証するために単体テストを作成しましょう。
func testAttemptRegistration() { registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.attemptUserRegistration() XCTAssert(registrationVM.loginSuccessful.value, 'Login must be successful') }
実行した場合、loginSuccessful
が原因で、このテストは失敗します。非同期networkService.attemptRegistration
まで値はtrueに設定されませんメソッドが終了しました。
モックを作成したのでNetworkServiceImpl
ここでattemptRegistration
メソッドは、正常な登録を返す前に1秒間待機します。GrandCentralDispatch(GCD)を使用するだけで、asyncAfter
を利用できます。 1秒後にアサートをチェックするメソッド。 GCDを追加した後asyncAfter
テストコードは次のようになります。
func testAttemptRegistration() { registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.passwordConfirmation = '123456' registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, 'Login must be successful') } }
注意を払っていれば、テストメソッドが実行されるため、これはまだ機能しないことがわかります 前 asyncAfter
ブロックが実行され、メソッドは 常に 結果として正常に合格します。幸い、XCTestException
がありますクラス。
XCTestException
を使用するようにメソッドを書き直してみましょうクラス:
func testAttemptRegistration() { let exp = expectation(description: 'Check registration attempt') registrationVM.emailAddress = ' [email protected] ' registrationVM.password = '123456' registrationVM.passwordConfirmation = '123456' registrationVM.attemptUserRegistration() DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { XCTAssert(self.registrationVM.loginSuccessful.value, 'Login must be successful') exp.fulfill() } waitForExpectations(timeout: 4.0) { error in if let _ = error { XCTAssert(false, 'Timeout while attempting a registration') } } }
RegistrationViewModel
をカバーする単体テストにより、新しい機能を追加したり、既存の機能を更新したりしても、何も壊れないという確信が持てるようになりました。
重要な注意点: ユニットテストは、対象となるメソッドの機能が変更されたときに更新されない場合、その価値を失います。単体テストの作成は、アプリケーションの残りの部分についていく必要があるプロセスです。
ヒント:ライティングテストを最後まで延期しないでください。開発中にテストを作成します。このようにして、何をテストする必要があるのか、そして何が国境のケースであるのかをよりよく理解することができます。
すべての単体テストが完全に開発され、正常に実行された後、コードの各ユニットが正しく機能していることを確信できますが、それはアプリケーション全体が意図したとおりに機能していることを意味しますか?
そこで登場するのが統合テストであり、その中でUIテストは不可欠なコンポーネントです。
UIテストを開始する前に、テストするUI要素とインタラクション(またはユーザーストーリー)がいくつかある必要があります。簡単なビューとそのビューコントローラを作成しましょう。
Main.storyboard
を開きます下の画像のような単純なViewControllerを作成します。
メールテキストフィールドタグを100、パスワードテキストフィールドタグを101、パスワード確認タグを102に設定します。
RegistrationViewController.swift
すべてのコンセントをストーリーボードに接続します。import UIKit class RegistrationViewController: UIViewController, UITextFieldDelegate { @IBOutlet weak var emailTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBOutlet weak var passwordConfirmationTextField: UITextField! @IBOutlet weak var registerButton: UIButton! private struct TextFieldTags { static let emailTextField = 100 static let passwordTextField = 101 static let confirmPasswordTextField = 102 } var viewModel: RegisterationViewModel? override func viewDidLoad() { super.viewDidLoad() emailTextField.delegate = self passwordTextField.delegate = self passwordConfirmationTextField.delegate = self bindViewModel() } }
ここに追加していますIBOutlets
およびTextFieldTags
クラスへの構造体。
これにより、編集中のテキストフィールドを特定できるようになります。ビューモデルの動的プロパティを利用するには、ビューコントローラで動的プロパティを「バインド」する必要があります。 bindViewModel
でそれを行うことができます方法:
fileprivate func bindViewModel() { if let viewModel = viewModel { viewModel.registrationEnabled.bindAndFire { self.registerButton.isEnabled = $0 } } }
次に、テキストフィールドのデリゲートメソッドを追加して、テキストフィールドのいずれかがいつ更新されているかを追跡します。
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { guard let viewModel = viewModel else { return true } let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string) switch textField.tag { case TextFieldTags.emailTextField: viewModel.emailAddress = newString case TextFieldTags.passwordTextField: viewModel.password = newString case TextFieldTags.confirmPasswordTextField: viewModel.passwordConfirmation = newString default: break } return true }
AppDelegate
ビューコントローラを適切なビューモデルにバインドします(この手順はMVVMアーキテクチャの要件であることに注意してください)。更新されたAppDelegate
コードは次のようになります。func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { initializeStartingView() return true } fileprivate func initializeStartingView() { if let rootViewController = window?.rootViewController as? RegistrationViewController { let networkService = NetworkServiceImpl() let viewModel = RegisterationViewModel(networkService: networkService) rootViewController.viewModel = viewModel } }
ストーリーボードファイルとRegistrationViewController
は本当にシンプルですが、自動化されたUIテストがどのように機能するかを示すには十分です。
すべてが正しく設定されている場合は、アプリの起動時に登録ボタンを無効にする必要があります。すべてのフィールドが入力されて有効な場合にのみ、登録ボタンを有効にする必要があります。
これを設定したら、最初のUIテストを作成できます。
UIテストでは、有効な電子メールアドレス、有効なパスワード、および有効なパスワードの確認がすべて入力された場合にのみ、[登録]ボタンが有効になるかどうかを確認する必要があります。これを設定する方法は次のとおりです。
TestingIOSUITests.swift
を開きますファイル。testExample()
を削除しますメソッドを追加し、testRegistrationButtonEnabled()
を追加します方法。testRegistrationButtonEnabled
にカーソルを置きますあなたがそこに何かを書くつもりのような方法。
この機能を使用してすべてのUI命令を記録できますが、簡単な命令を手動で作成する方がはるかに高速である場合があります。
これは、パスワードのテキストフィールドをタップしてメールアドレスを入力するためのレコーダーの指示の例です。 [メール保護] '
let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ')
XCTAsserts
を追加できます。アプリケーションまたはUI要素のさまざまな状態をテストします。
記録された指示は必ずしも自明であるとは限らず、テスト方法全体を読みにくく、理解しにくくすることさえあります。幸い、UI命令を手動で入力できます。
次のUI命令を手動で作成しましょう。
UI要素を参照するには、プレースホルダー識別子を使用できます。プレースホルダー識別子は、ストーリーボードの[ユーザー補助]の下の[IDインスペクター]ペインで設定できます。パスワードテキストフィールドのユーザー補助識別子を「passwordTextField」に設定します。
パスワードUIインタラクションは、次のように記述できます。
let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password')
UIインタラクションがもう1つ残っています。それは、パスワード入力の確認インタラクションです。今回は、プレースホルダーでパスワードの確認テキストフィールドを参照します。ストーリーボードに移動し、パスワードの確認テキストフィールドに「パスワードの確認」プレースホルダーを追加します。これで、ユーザーインタラクションは次のように記述できます。
let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('password')
これで、必要なUIインタラクションがすべて揃ったら、あとは簡単なXCTAssert
を書くだけです。 (単体テストで行ったのと同じ)[登録]ボタンのisEnabled
かどうかを確認します状態はtrueに設定されます。登録ボタンは、そのタイトルを使用して参照できます。ボタンのisEnabled
を確認するためにアサートしますプロパティは次のようになります。
let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled')
UIテスト全体は次のようになります。
func testRegistrationButtonEnabled() { // Recorded by Xcode let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ') // Queried by accessibility identifier let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password') // Queried by placeholder text let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('password') let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled') }
テストが実行されると、Xcodeはシミュレーターを起動し、テストアプリケーションを起動します。アプリケーションが起動した後、UIインタラクション命令が1つずつ実行され、最後にアサートが正常にアサートされます。
テストを改善するために、isEnabled
もテストしてみましょう。必須フィールドのいずれかが正しく入力されていない場合、登録ボタンのプロパティはfalseです。
完全なテストメソッドは次のようになります。
func testRegistrationButtonEnabled() { let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == false, 'Registration button should be disabled') // Recorded by Xcode let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ') XCTAssert(registerButton.isEnabled == false, 'Registration button should be disabled') // Queried by accessibility identifier let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password') XCTAssert(registerButton.isEnabled == false, 'Registration button should be disabled') // Queried by placeholder text let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('pass') XCTAssert(registerButton.isEnabled == false, 'Registration button should be disabled') confirmPasswordTextField.typeText('word') // the whole confirm password word will now be 'password' XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled') }
ヒント:UI要素を識別するための推奨される方法は、アクセシビリティ識別子を使用することです。名前、プレースホルダー、またはローカライズ可能なその他のプロパティが使用されている場合、別の言語が使用されていると要素は見つかりません。その場合、テストは失敗します。
UIテストの例は非常に単純ですが、自動化されたUIテストの威力を示しています。
Xcodeに含まれているUIテストフレームワークのすべての可能性(および多くの可能性)を発見する最良の方法は、プロジェクトでUIテストの作成を開始することです。示されているような単純なユーザーストーリーから始めて、ゆっくりとより複雑なストーリーとテストに移ります。
私の経験から、良いテストを学び、書き込もうとすると、開発の他の側面について考えるようになります。それはあなたがより良くなるのを助けます iOS開発者 完全に。
優れたテストを作成するには、コードをより適切に整理する方法を学ぶ必要があります。
組織化されたモジュール式の適切に記述されたコードは、ストレスのないユニットおよびUIテストを成功させるための主な要件です。
場合によっては、コードが適切に編成されていないと、テストを作成できないことさえあります。
アプリケーションの構造とコードの編成について考えると、MVVM、MVP、VIPER、またはその他のそのようなパターンを使用することで、コードがより適切に構造化され、モジュール化され、テストが容易になることがわかります(Massive View Controllerの問題も回避できます)。 。
テストを作成するときは、間違いなく、ある時点で、モッククラスを作成する必要があります。依存性注入の原則とプロトコル指向のコーディング手法について考え、学ぶことができます。これらの原則を理解して使用することで、将来のプロジェクトのコード品質が大幅に向上します。
テストを書き始めると、コードを書くときにコーナーケースとエッジ条件についてもっと考えていることに気付くでしょう。これは、バグになる前に起こりうるバグを排除するのに役立ちます。メソッドの考えられる問題とネガティブな結果について考えると、ポジティブな結果をテストするだけでなく、ネガティブな結果もテストし始めます。
ご覧のとおり、単体テストはさまざまな開発の側面に影響を与える可能性があり、優れた単体テストとUIテストを作成することで、より優れた幸せな開発者になる可能性があります(バグの修正に多くの時間を費やす必要はありません)。
自動テストの作成を開始すると、最終的には自動テストのメリットがわかります。あなたがそれを自分で見るとき、あなたはその最強の支持者になるでしょう。
次に、テキストフィールドのデリゲートメソッドを追加して、テキストフィールドのいずれかがいつ更新されているかを追跡します。
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { guard let viewModel = viewModel else { return true } let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string) switch textField.tag { case TextFieldTags.emailTextField: viewModel.emailAddress = newString case TextFieldTags.passwordTextField: viewModel.password = newString case TextFieldTags.confirmPasswordTextField: viewModel.passwordConfirmation = newString default: break } return true }
AppDelegate
ビューコントローラを適切なビューモデルにバインドします(この手順はMVVMアーキテクチャの要件であることに注意してください)。更新されたAppDelegate
コードは次のようになります。func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { initializeStartingView() return true } fileprivate func initializeStartingView() { if let rootViewController = window?.rootViewController as? RegistrationViewController { let networkService = NetworkServiceImpl() let viewModel = RegisterationViewModel(networkService: networkService) rootViewController.viewModel = viewModel } }
ストーリーボードファイルとRegistrationViewController
は本当にシンプルですが、自動化されたUIテストがどのように機能するかを示すには十分です。
すべてが正しく設定されている場合は、アプリの起動時に登録ボタンを無効にする必要があります。すべてのフィールドが入力されて有効な場合にのみ、登録ボタンを有効にする必要があります。
これを設定したら、最初のUIテストを作成できます。
UIテストでは、有効な電子メールアドレス、有効なパスワード、および有効なパスワードの確認がすべて入力された場合にのみ、[登録]ボタンが有効になるかどうかを確認する必要があります。これを設定する方法は次のとおりです。
TestingIOSUITests.swift
を開きますファイル。testExample()
を削除しますメソッドを追加し、testRegistrationButtonEnabled()
を追加します方法。testRegistrationButtonEnabled
にカーソルを置きますあなたがそこに何かを書くつもりのような方法。
この機能を使用してすべてのUI命令を記録できますが、簡単な命令を手動で作成する方がはるかに高速である場合があります。
これは、パスワードのテキストフィールドをタップしてメールアドレスを入力するためのレコーダーの指示の例です。 [メール保護] '
let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ')
XCTAsserts
を追加できます。アプリケーションまたはUI要素のさまざまな状態をテストします。
記録された指示は必ずしも自明であるとは限らず、テスト方法全体を読みにくく、理解しにくくすることさえあります。幸い、UI命令を手動で入力できます。
次のUI命令を手動で作成しましょう。
UI要素を参照するには、プレースホルダー識別子を使用できます。プレースホルダー識別子は、ストーリーボードの[ユーザー補助]の下の[IDインスペクター]ペインで設定できます。パスワードテキストフィールドのユーザー補助識別子を「passwordTextField」に設定します。
パスワードUIインタラクションは、次のように記述できます。
let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password')
UIインタラクションがもう1つ残っています。それは、パスワード入力の確認インタラクションです。今回は、プレースホルダーでパスワードの確認テキストフィールドを参照します。ストーリーボードに移動し、パスワードの確認テキストフィールドに「パスワードの確認」プレースホルダーを追加します。これで、ユーザーインタラクションは次のように記述できます。
let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('password')
これで、必要なUIインタラクションがすべて揃ったら、あとは簡単なXCTAssert
を書くだけです。 (単体テストで行ったのと同じ)[登録]ボタンのisEnabled
かどうかを確認します状態はtrueに設定されます。登録ボタンは、そのタイトルを使用して参照できます。ボタンのisEnabled
を確認するためにアサートしますプロパティは次のようになります。
let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled')
UIテスト全体は次のようになります。
func testRegistrationButtonEnabled() { // Recorded by Xcode let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ') // Queried by accessibility identifier let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password') // Queried by placeholder text let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('password') let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled') }
テストが実行されると、Xcodeはシミュレーターを起動し、テストアプリケーションを起動します。アプリケーションが起動した後、UIインタラクション命令が1つずつ実行され、最後にアサートが正常にアサートされます。
テストを改善するために、isEnabled
もテストしてみましょう。必須フィールドのいずれかが正しく入力されていない場合、登録ボタンのプロパティはfalseです。
完全なテストメソッドは次のようになります。
func testRegistrationButtonEnabled() { let registerButton = XCUIApplication().buttons['REGISTER'] XCTAssert(registerButton.isEnabled == false, 'Registration button should be disabled') // Recorded by Xcode let emailTextField = XCUIApplication().otherElements.containing(.staticText, identifier:'Email Address').children(matching: .textField).element emailTextField.tap() emailTextField.typeText(' [email protected] ') XCTAssert(registerButton.isEnabled == false, 'Registration button should be disabled') // Queried by accessibility identifier let passwordTextField = XCUIApplication().secureTextFields['passwordTextField'] passwordTextField.tap() passwordTextField.typeText('password') XCTAssert(registerButton.isEnabled == false, 'Registration button should be disabled') // Queried by placeholder text let confirmPasswordTextField = XCUIApplication().secureTextFields['Confirm Password'] confirmPasswordTextField.tap() confirmPasswordTextField.typeText('pass') XCTAssert(registerButton.isEnabled == false, 'Registration button should be disabled') confirmPasswordTextField.typeText('word') // the whole confirm password word will now be 'password' XCTAssert(registerButton.isEnabled == true, 'Registration button should be enabled') }
ヒント:UI要素を識別するための推奨される方法は、アクセシビリティ識別子を使用することです。名前、プレースホルダー、またはローカライズ可能なその他のプロパティが使用されている場合、別の言語が使用されていると要素は見つかりません。その場合、テストは失敗します。
UIテストの例は非常に単純ですが、自動化されたUIテストの威力を示しています。
Xcodeに含まれているUIテストフレームワークのすべての可能性(および多くの可能性)を発見する最良の方法は、プロジェクトでUIテストの作成を開始することです。示されているような単純なユーザーストーリーから始めて、ゆっくりとより複雑なストーリーとテストに移ります。
私の経験から、良いテストを学び、書き込もうとすると、開発の他の側面について考えるようになります。それはあなたがより良くなるのを助けます iOS開発者 完全に。
総アドレス可能市場とは何ですか
優れたテストを作成するには、コードをより適切に整理する方法を学ぶ必要があります。
組織化されたモジュール式の適切に記述されたコードは、ストレスのないユニットおよびUIテストを成功させるための主な要件です。
場合によっては、コードが適切に編成されていないと、テストを作成できないことさえあります。
アプリケーションの構造とコードの編成について考えると、MVVM、MVP、VIPER、またはその他のそのようなパターンを使用することで、コードがより適切に構造化され、モジュール化され、テストが容易になることがわかります(Massive View Controllerの問題も回避できます)。 。
テストを作成するときは、間違いなく、ある時点で、モッククラスを作成する必要があります。依存性注入の原則とプロトコル指向のコーディング手法について考え、学ぶことができます。これらの原則を理解して使用することで、将来のプロジェクトのコード品質が大幅に向上します。
テストを書き始めると、コードを書くときにコーナーケースとエッジ条件についてもっと考えていることに気付くでしょう。これは、バグになる前に起こりうるバグを排除するのに役立ちます。メソッドの考えられる問題とネガティブな結果について考えると、ポジティブな結果をテストするだけでなく、ネガティブな結果もテストし始めます。
ご覧のとおり、単体テストはさまざまな開発の側面に影響を与える可能性があり、優れた単体テストとUIテストを作成することで、より優れた幸せな開発者になる可能性があります(バグの修正に多くの時間を費やす必要はありません)。
自動テストの作成を開始すると、最終的には自動テストのメリットがわかります。あなたがそれを自分で見るとき、あなたはその最強の支持者になるでしょう。