テストを書くことは華やかではありませんが、テストはあなたのきらめくアプリがバグだらけのガラクタにならないようにするので、必要なことです。 このチュートリアルを読んでいる場合、コードと UI のテストを書くべきであることをすでに知っていますが、どのように書けばよいかわからないかもしれません。 おそらく、すでにテストが書かれていますが、それが正しいテストであるかどうか確信が持てません。 または、新しいアプリに取り組み始めていて、進めながらテストを行いたい場合。
このチュートリアルでは、次のことを説明します。
- Xcode のテスト ナビゲーターを使ってアプリのモデルと非同期メソッドをテストする方法
- スタブやモックを使ってライブラリやシステム オブジェクトとのやり取りを偽装する方法
- UI とパフォーマンスのテスト方法
- コード カバー ツールの使用方法
途中で、次のことがわかるようになります。 テスト忍者が使う単語をいくつかピックアップします。
Figuring Out What to Test
テストを書く前に、基本を知ることが重要です。 何をテストする必要があるのでしょうか。
目標が既存のアプリを拡張することである場合、まず、変更する予定のすべてのコンポーネントのテストを書くべきです。
一般に、テストは次の項目をカバーする必要があります。 モデル クラスとメソッド、およびコントローラとそれらの相互作用
Best Practices for Testing
FIRST という略語は有効なユニット テストに関する一連の基準を簡潔に説明したものである。 これらの基準は次のとおりです:
- 速い。
- 独立/分離:テストはすばやく実行されなければなりません。
- 反復可能であること。 テストを実行するたびに同じ結果を得る必要があります。 外部のデータ プロバイダーや同時実行の問題により、断続的に失敗することがあります。
- 自己検証。 テストは完全に自動化されるべきです。 ログファイルのプログラマの解釈に頼るのではなく、出力は「合格」か「不合格」のどちらかでなければなりません。 理想的には、テストはテストする本番コードを書く前に書くべきです (テスト駆動開発)。
FIRSTの原則に従うことにより、テストがアプリの障害になるのではなく、明確で有用なものになります。 2 つの別々のスターター プロジェクトがあります。 BullsEye と HalfTunes です。
- BullsEye は、iOS Apprentice のサンプル アプリをベースにしています。 ゲーム ロジックは
BullsEyeGame
クラスにあり、このチュートリアルでテストします。 - HalfTunes は、URLSession チュートリアルからのサンプル アプリの更新版です。 ユーザーは iTunes API に楽曲をクエリし、楽曲の断片をダウンロードして再生できます。
Unit Testing in Xcode
The Test navigator provides the easiest way to work with tests; you will use it to create test targets and run tests against your app.
Creating a Unit Test Target
BullsEyeプロジェクトを開き、command-6 を押して Test navigatorを開きます。
左下にある + ボタンをクリックし、メニューから New Unit Test Target…
デフォルト名、BlsEyeTestsを受け付けます。 テストバンドルがテストナビゲーターに表示されたら、クリックしてエディターでバンドルを開きます。 バンドルが自動的に表示されない場合は、他のナビゲータのいずれかをクリックしてトラブルシューティングを行い、テスト ナビゲータに戻ります。
デフォルトのテンプレートは、テスト フレームワーク XCTest をインポートし、setUp()
、tearDown()
、およびサンプル テスト メソッドを含む XCTestCase
のサブクラスを定義しています。 これらはどちらもすべてのテスト クラスを実行します。
また、テスト ナビゲーターまたはガッターで、そのダイヤモンドをクリックして、個々のテスト メソッドを実行することもできます。 サンプル テストはまだ何もしていないので、非常に速く実行されます!
すべてのテストが成功すると、ダイヤモンドが緑色に変わり、チェックマークが表示されます。 testPerformanceExample()
の最後にある灰色の菱形をクリックすると、パフォーマンス結果を開くことができます:
このチュートリアルでは testPerformanceExample()
と testExample()
は必要ないので、削除します。
Using XCTAssert to Test Models
最初に、XCTAssert
関数を使って BullsEye モデルのコア機能をテストしてみましょう。
BullsEyeTests.swiftのimport
ステートメントのすぐ下に、次の行を追加します:
@testable import BullsEye
これにより、ユニットテストはBullsEyeの内部型と関数にアクセスできます。
BullsEyeTests
クラスの先頭で、次のプロパティを追加します。
var sut: BullsEyeGame!
これはBullsEyeGame
のプレースホルダーを作成し、テスト対象のシステム(SUT)、またはこのテストケースクラスがテスト対象としているオブジェクトを示します。
次に、setup()
の内容を次のように置き換えます。
super.setUp()sut = BullsEyeGame()sut.startNewGame()
これはクラス レベルで BullsEyeGame
オブジェクトを作成するので、このテスト クラスのすべてのテストは SUT オブジェクトのプロパティとメソッドにアクセスできます。
忘れないうちに、tearDown()
でSUTオブジェクトを解放しておきましょう。 その内容を次のように置き換えます:
sut = nilsuper.tearDown()
setUp()
で SUT を作成し tearDown()
でそれを解放することは良い習慣です。 詳細については、この件に関する Jon Reid の投稿を確認してください。最初のテストを書く
さて、最初のテストを書く準備ができました!
BullsEyeTests
の最後に次のコードを追加します。
func testScoreIsComputed() { // 1. given let guess = sut.targetValue + 5 // 2. when sut.check(guess: guess) // 3. then XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")}
テスト メソッドの名前は常に test で始まり、何をテストするかの説明が続きます。 ここでは、必要なすべての値を設定します。 この例では、guess
値を作成し、targetValue
との違いを指定します。
check(guess:)
.sut.scoreRound
は95 (100 – 5)になるはずです。ガターまたはテストナビゲータでダイヤモンドのアイコンをクリックして、テストを実行します。 これにより、アプリがビルドされて実行され、ダイヤモンドのアイコンが緑のチェックマークに変わります!
Debugging a Test
意図的に BullsEyeGame
に組み込まれたバグがあり、今それを見つける練習をしています。 そのバグを実際に見るために、与えられたセクションの targetValue
から 5 を引き、他は同じにするテストを作成します。
以下のテストを追加してください。
func testScoreIsComputedWhenGuessLTTarget() { // 1. given let guess = sut.targetValue - 5 // 2. when sut.check(guess: guess) // 3. then XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")}
guess
とtargetValue
の差はまだ5なので、スコアはまだ95のはずです。
ブレークポイントナビで、[テスト失敗]ブレークポイントを追加します。
テストを実行し、テスト失敗の XCTAssertEqual
行で停止するはずです。
デバッグコンソールでsut
とguess
を調べます。
guess
は targetValue - 5
ですが scoreRound
は95ではなく105です!
さらに調べるには、通常のデバッグプロセスを使用します。 また、BullsEyeGame.swift の check(guess:)
内、difference
を生成している箇所にもブレークポイントを設定します。 その後、再度テストを実行し、let difference
ステートメントをステップオーバーして、アプリ内のdifference
の値を検査します。
問題は、difference
が負なので、100 – (-5) になることです。 これを解決するには、difference
の絶対値を使用する必要があります。 check(guess:)
において、正しい行のコメントを解除し、間違った行を削除します。
2つのブレークポイントを削除し、テストを再度実行して、成功したことを確認します。
Using XCTestExpectation to Test Asynchronous Operations
モデルのテスト方法とテストの失敗のデバッグ方法を学んだので、次は非同期のコードのテストに移ります。 これは URLSession
を使用して、iTunes API に問い合わせ、曲のサンプルをダウンロードします。 ネットワーク操作のために AlamoFire を使用するように変更したいとします。 何かが壊れているかどうかを確認するために、ネットワーク操作のテストを書き、コードを変更する前と後にそれらを実行する必要があります。
URLSession
メソッドは非同期です:それらはすぐに返されますが、後で実行を終了しません。 非同期メソッドをテストするには、XCTestExpectation
を使用して、テストが非同期操作が完了するのを待ちます。
非同期テストは通常遅いので、より速いユニットテストからそれらを分離しておく必要があります。 HalfTunesSlowTests
クラスを開き、既存の import
ステートメントのすぐ下にある HalfTunes app モジュールをインポートします。
@testable import HalfTunes
このクラスのすべてのテストは、Apple のサーバーにリクエストを送信するのにデフォルトの URLSession
を使用するので、sut
オブジェクトを宣言し、setUp()
でそれを作成し、tearDown()
でそれをリリースしてください。
var sut: URLSession!override func setUp() { super.setUp() sut = URLSession(configuration: .default)}override func tearDown() { sut = nil super.tearDown()}
次に、この非同期テストを追加します。
// Asynchronous test: success fast, failure slowfunc testValidCallToiTunesGetsHTTPStatusCode200() { // given let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") // 1 let promise = expectation(description: "Status code: 200") // when let dataTask = sut.dataTask(with: url!) { data, response, error in // then if let error = error { XCTFail("Error: \(error.localizedDescription)") return } else if let statusCode = (response as? HTTPURLResponse)?.statusCode { if statusCode == 200 { // 2 promise.fulfill() } else { XCTFail("Status code: \(statusCode)") } } } dataTask.resume() // 3 wait(for: , timeout: 5)}
このテストは、iTunesに有効なクエリを送信すると200のステータスコードが返ってくることを確認します。 コードのほとんどは、アプリで記述するものと同じですが、以下の行が追加されています:
- expectation(description:):
promise
に格納されているXCTestExpectation
オブジェクトを返します。description
パラメータには、何が起こるかを期待する内容を記述します。 - promise.fulfill(): 非同期メソッドの完了ハンドラの成功条件クロージャでこれを呼び出すと、期待値が満たされたことを示すフラグが立てられます。
- wait(for:timeout:): すべての期待値が満たされるか、
timeout
間隔が終了するか、どちらか先に起こるまでテストを実行し続けます。
テストを実行します。 インターネットに接続している場合、シミュレーターでアプリをロードしてから、テストが成功するまで約 1 秒かかるはずです。
Failing Fast
失敗は痛いですが、永遠にかかる必要はありません。
失敗を体験するには、URL の “itunes” から ‘s’ を削除するだけです。 失敗しましたが、タイムアウトの間隔をフルに使っています! これは、リクエストが必ず成功すると想定し、そこでpromise.fulfill()
を呼び出したからです。
これを改善し、仮定を変更することによって、テストをより速く失敗させることができます。 要求が成功するのを待つのではなく、非同期メソッドの完了ハンドラが呼び出されるまでだけ待ちます。 これは、アプリがサーバーからレスポンス (OKかエラー) を受け取るとすぐに起こります。
これがどのように機能するかを見るために、新しいテストを作成します。
しかし、最初に、url
に加えた変更を元に戻して、前のテストを修正します。
次に、次のテストをクラスに追加します。 リクエストが失敗した場合、then
アサーションは失敗します。
テストを実行します。 今度は失敗するまでに約 1 秒かかるはずです。
url
を修正し、テストを再度実行して、成功したことを確認します。
Faking Objects and Interactions
Asynchronous tests は、あなたのコードが非同期 API に正しい入力を生成することに確信を与えてくれます。 また、コードが URLSession
から入力を受け取ったときに正しく動作すること、または、ユーザーのデフォルト データベースまたは iCloud コンテナを正しく更新することをテストしたいと思うかもしれません。
ほとんどのアプリはシステムまたはライブラリ オブジェクト (ユーザーが制御できないオブジェクト) とやり取りし、これらのオブジェクトとやり取りするテストは遅く、再現性がないことがあり、FIRST 原則の 2 つに違反します。 代わりに、スタブから入力を取得するか、モック オブジェクトを更新することにより、相互作用を偽造できます。
コードがシステムまたはライブラリ オブジェクトに依存している場合は、偽造を使用します。 これは、その部分を演じる偽のオブジェクトを作成し、この偽物をコードに注入することによって行うことができます。 Jon Reid による Dependency Injection では、これを行うためのいくつかの方法を説明しています。
Fake Input From Stub
このテストでは、searchResults.count
が正しいことをチェックすることにより、アプリの updateSearchResults(_:)
がセッションによってダウンロードされたデータを正しくパースすることを確認します。 SUT はビュー コントローラーで、スタブといくつかの事前にダウンロードされたデータでセッションをフェイクします。 それをHalfTunesFakeTestsと名付けます。 HalfTunesFakeTestsを開いてください。
@testable import HalfTunes
ここで、HalfTunesFakeTests
クラスの内容を次のように置き換えます:
var sut: SearchViewController!override func setUp() { super.setUp() sut = UIStoryboard(name: "Main", bundle: nil) .instantiateInitialViewController() as? SearchViewController}override func tearDown() { sut = nil super.tearDown()}
これは、SearchViewController
であるSUTを宣言し、setUp()
でそれを作成し、tearDown()
でそれをリリースします。
次に、偽のセッションがテストに提供するいくつかのサンプル JSON データが必要になります。 いくつかの項目で十分なので、iTunes でのダウンロード結果を制限するために、URL 文字列に &limit=3
を追加してください:
https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3
この URL をコピーして、ブラウザに貼り付けてください。 1.txtや1.txt.jsといった名前のファイルがダウンロードされます。 プレビューしてJSONファイルであることを確認し、abbaData.jsonにリネームします。
ここで、Xcodeに戻って、プロジェクトナビゲータに移動します。 HalfTunesFakeTestsグループにファイルを追加します。
HalfTunesプロジェクトは、サポートファイルであるDHURLSessionMock.swiftを含んでいます。 これは、URL
またはURLRequest
を持つデータタスクを作成するメソッド(スタブ)と、DHURLSession
というシンプルなプロトコルを定義しています。 また、データ、レスポンス、エラーを選択したモック URLSession
オブジェクトを作成できる初期化子を持つ、このプロトコルに準拠した URLSessionMock
も定義されています。
フェイクを設定するには、HalfTunesFakeTests.swift に移動して、setUp()
内、SUT を作成する文の後に以下を追加してください。
let testBundle = Bundle(for: type(of: self))let path = testBundle.path(forResource: "abbaData", ofType: "json")let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")let urlResponse = HTTPURLResponse( url: url!, statusCode: 200, httpVersion: nil, headerFields: nil)let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)sut.defaultSession = sessionMock
これにより、偽のデータとレスポンスが設定され、偽のセッションオブジェクトが作成されます。
さて、updateSearchResults(_:)
を呼び出すことで偽のデータが解析されるかどうかをチェックするテストを書く準備が整いました。
func test_UpdateSearchResults_ParsesData() { // given let promise = expectation(description: "Status code: 200") // when XCTAssertEqual( sut.searchResults.count, 0, "searchResults should be empty before the data task runs") let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba") let dataTask = sut.defaultSession.dataTask(with: url!) { data, response, error in // if HTTP request is successful, call updateSearchResults(_:) // which parses the response data into Tracks if let error = error { print(error.localizedDescription) } else if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { self.sut.updateSearchResults(data) } promise.fulfill() } dataTask.resume() wait(for: , timeout: 5) // then XCTAssertEqual(sut.searchResults.count, 3, "Didn't parse 3 items from fake response")}
スタブが非同期メソッドのふりをしているので、やはり非同期テストとして書かなければなりません。
whenアサーションは、データタスクが実行される前にsearchResults
が空であるというものです。
偽のデータには 3 つの Track
オブジェクトの JSON が含まれているので、then アサーションはビューコントローラーの searchResults
配列に 3 つのアイテムが含まれているということです。 実際のネットワーク接続がないので、かなり早く成功するはずです!
モック オブジェクトへの偽の更新
前のテストでは、偽のオブジェクトからの入力を提供するスタブを使用しました。 次に、モック オブジェクトを使用して、コードが UserDefaults
を正しく更新することをテストします。
BullsEyeプロジェクトを再度開きます。 このアプリには2つのゲームスタイルがあります。 ユーザーはスライダーを動かして目標値に一致させるか、スライダーの位置から目標値を推測します。
次のテストでは、アプリが gameStyle
プロパティを正しく保存することを確認します。
テスト ナビゲーターで、新しいユニット テスト クラスをクリックし、それを BullsEyeMockTests と名付けます。 import
ステートメントの下に以下を追加します。
@testable import BullsEyeclass MockUserDefaults: UserDefaults { var gameStyleChanged = 0 override func set(_ value: Int, forKey defaultName: String) { if defaultName == "gameStyle" { gameStyleChanged += 1 } }}
MockUserDefaults
overrides set(_:forKey:)
to increment the gameStyleChanged
flag. 多くの場合、Bool
変数を設定する同様のテストを見かけますが、Int
をインクリメントすることで、より柔軟なテストができます – たとえば、メソッドが 1 回だけ呼び出されることをチェックできます。
BullsEyeMockTests
で SUT とモック オブジェクトを宣言します。
var sut: ViewController!var mockUserDefaults: MockUserDefaults!
次に、デフォルトの setUp()
と tearDown()
を次のように置き換えます。
override func setUp() { super.setUp() sut = UIStoryboard(name: "Main", bundle: nil) .instantiateInitialViewController() as? ViewController mockUserDefaults = MockUserDefaults(suiteName: "testing") sut.defaults = mockUserDefaults}override func tearDown() { sut = nil mockUserDefaults = nil super.tearDown()}
これは SUT とモック オブジェクトを作成し、SUT のプロパティとしてモック オブジェクトをインジェクトするものです。
さて、テンプレート内の 2 つのデフォルト テスト メソッドを次のように置き換えます:
func testGameStyleCanBeChanged() { // given let segmentedControl = UISegmentedControl() // when XCTAssertEqual( mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions") segmentedControl.addTarget(sut, action: #selector(ViewController.chooseGameStyle(_:)), for: .valueChanged) segmentedControl.sendActions(for: .valueChanged) // then XCTAssertEqual( mockUserDefaults.gameStyleChanged, 1, "gameStyle user default wasn't changed")}
アサーションは、テスト メソッドが分割された制御を変更する前に gameStyleChanged
フラグが 0 であるというものです。
Xcode における UI テスト
UI テストでは、ユーザー インターフェイスとのインタラクションをテストできます。 UI テストは、クエリでアプリケーションの UI オブジェクトを見つけ、イベントを合成し、次にそれらのオブジェクトにイベントを送信することによって動作します。
BullsEye プロジェクトの Test ナビゲーターで、新しい UI テスト ターゲットを追加します。
BullsEyeUITests.swiftを開き、BullsEyeUITests
クラスの先頭にこのプロパティを追加します:
var app: XCUIApplication!
setUp()
で、XCUIApplication().launch()
を次の記述と入れ替えます:
app = XCUIApplication()app.launch()
testExample()
の名前をtestGameStyleSwitch()
と変更します。
testGameStyleSwitch()
で新しい行を開き、エディター ウィンドウの下部にある赤い[記録]ボタンをクリックします:
これにより、シミュレーターでアプリが開き、テスト コマンドとして操作を記録するモードが適用されます。 アプリをロードしたら、ゲーム スタイル スイッチのスライド セグメントと上部ラベルをタップします。
You now have the following three lines in testGameStyleSwitch()
:
let app = XCUIApplication()app.buttons.tap()app.staticTexts.tap()
The Recorder has created code to test the same actions you tested in the app.Xcode Record button to stop the recording.Xcode Record buttonをクリックして、記録を停止してください。 スライダーとラベルにタップを送信します。 これらをベースにして、独自の UI テストを作成します。
その他の記述があれば、削除してください。
1 行目は、setUp()
で作成したプロパティと重複しているので、その行を削除してください。 まだ何もタップする必要はありませんので、2行目と3行目の末尾の.tap()
も削除してください。 の横にある小さなメニューを開き、
segmentedControls.buttons
.
を選択すると、次のようになります。
app.segmentedControls.buttonsapp.staticTexts
その他のオブジェクトをタップして、テストでアクセスできるコードをレコーダーに表示させます。
// givenlet slideButton = app.segmentedControls.buttonslet typeButton = app.segmentedControls.buttonslet slideLabel = app.staticTextslet typeLabel = app.staticTexts
セグメント化されたコントロールの 2 つのボタンの名前と、考えられる 2 つのトップ ラベルがわかったので、次のコードを下に追加します:
// thenif slideButton.isSelected { XCTAssertTrue(slideLabel.exists) XCTAssertFalse(typeLabel.exists) typeButton.tap() XCTAssertTrue(typeLabel.exists) XCTAssertFalse(slideLabel.exists)} else if typeButton.isSelected { XCTAssertTrue(typeLabel.exists) XCTAssertFalse(slideLabel.exists) slideButton.tap() XCTAssertTrue(slideLabel.exists) XCTAssertFalse(typeLabel.exists)}
これは、セグメント化コントロールのそれぞれのボタンに tap()
したときに正しいラベルがあるかどうかを確認するものです。 テストを実行します – すべてのアサーションが成功するはずです。
パフォーマンス テスト
Apple のドキュメントから。 パフォーマンス テストでは、評価したいコードのブロックを取り、それを 10 回実行し、平均実行時間と実行の標準偏差を収集します。 これらの個々の測定値の平均は、テスト実行の値を形成し、成功または失敗を評価するためにベースラインと比較することができます。
これを実際に見るには、HalfTunes プロジェクトを再び開き、HalfTunesFakeTests.Tests.Test にある、HalfTunesFakeTests.Test にある、measure()
のクロージャに測定したいコードを配置するだけでよい。
func test_StartDownload_Performance() { let track = Track( name: "Waterloo", artist: "ABBA", previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a") measure { self.sut.startDownload(track) }}
テストを実行し、統計情報を見るために、measure()
末尾の閉鎖の始まりの隣に表示されているアイコンをクリックします。 その後、パフォーマンス テストを再度実行し、結果を表示します – ベースラインよりも良くなっているかもしれないし、悪くなっているかもしれません。 ベースラインはデバイス構成ごとに保存されるので、複数の異なるデバイスで同じテストを実行し、特定の構成のプロセッサ速度、メモリなどに依存する異なるベースラインをそれぞれが維持することが可能です。
コード カバレッジ
コード カバレッジ ツールは、テストによって実際に実行されるアプリ コードを教えてくれるので、アプリ コードのどの部分が(まだ)テストされていないかがわかります。
コード カバレッジを有効にするには、スキームの Test アクションを編集し、[オプション] タブの [カバレッジを収集する] チェック ボックスをオンにします。
SearchViewController.swift 内の関数とクロージャーのリストを表示するには、三角形をクリックします:
updateSearchResults(_:)
までスクロールすると、カバー率が 87.9% であることがわかります。 右側のサイドバーのカバレッジ注釈にマウスを合わせると、コードのセクションが緑または赤でハイライト表示されます。
カバレッジ注釈は、テストが各コード セクションに何回当たったかを示し、呼ばれなかったセクションは赤でハイライト表示されます。
この関数のカバレッジを上げるには、abbaData.json を複製し、異なるエラーを引き起こすように編集することができます。 たとえば、print("Results key not found in dictionary")
をヒットするテストでは "results"
を "result"
に変更します。
100% Coverage?
どの程度コードカバレッジ 100% を目指すべきですか? 100% unit test coverage」でググると、「100% coverage」の定義そのものをめぐる議論とともに、さまざまな賛成論や反対論が見つかります。 反対論者は、最後の10-15%は努力に値しないと言います。 賛成派は、最後の10-15%が最も重要で、テストするのがとても難しいからだと言います。 テストできないコードはより深い設計上の問題の兆候であるという説得力のある議論を見つけるには、「hard to unit test bad design」でググってください。 この iOS ユニット テストと UI テストのチュートリアルが、あらゆるものをテストする自信を与えてくれたことを願っています!
このチュートリアルの上部または下部にある「Download Materials」ボタンを使用して、プロジェクトの完成版をダウンロードすることができます。 あなた自身のテストを追加して、スキルを伸ばし続けてください!
さらなる学習のためのリソースがいくつかあります:
- テストのトピックに関するいくつかの WWDC ビデオがあります。 WWDC17 のもので、良いものが 2 つあります。 Engineering for Testability」と「Testing Tips & Tricks」です。
- 次のステップは、自動化です。 継続的インテグレーションと継続的デリバリーです。 Apple の Automating the Test Process with Xcode Server と
xcodebuild
、および ThoughtWorks の専門知識を活用した Wikipedia の継続的デリバリの記事から始めてください。 - すでにアプリを持っているがまだそのためのテストを書いていない場合は、マイケル フェザーズによる Working Effectively with Legacy Code を参照するとよいでしょう、テストのないコードはレガシ コードですから!
- ジョン リードの Quality Coding サンプル アプリ アーカイブはテスト駆動開発についてもっと学ぶには素晴らしいものです。
raywenderlich.com Weekly
the raywenderlich.com newsletter は、モバイル開発者として知っておくべきすべてのことについて最新情報を入手する最も簡単な方法です。
当社のチュートリアルやコースのダイジェストを毎週受け取り、無料の詳細メール コースを特典として受け取ってください!
Raywenderlich.com Weekly は、モバイル開発者であるあなたが知りたいことについての最新情報を入手できる最も簡単な方法を提供します。
Average Rating
4.7/5
Add a rating for this content
Sign in to add a rating