Matteo Manferdini

多くの開発者はユニット テストが分かりにくいと感じています。 これは、ネットワーク リクエストのようなクラスまたは非同期コードをテストするために必要な高度なテクニックによって、さらに悪化します。 それを理解すれば、ユニットテストはもっと身近なものになります。

Contents

  • Unit testing explained in simple.Unity testing,
  • ユニットテストにより、到達しにくいエッジケースをチェックし、バグを防ぐことができます
  • Xcodeでユニットテストを書く
  • 優れたアーキテクチャはコードをテストしやすくします
  • ユニットテストを簡単にするために純粋関数として値タイプを扱う
  • ユニットテストが完了した後、ユニットテストを開始します。
  • XCTest フレームワークのアサーション関数を使ってコードを検証する
  • Measuring code coverage of a test in Xcode
  • Why aiming for specific code coverage is meaningless and what should do instead

ユニットテストを簡単に説明する。 9240>

アプリケーションを書くときは、コードを書き、それを実行し、意図したとおりに動作するかどうかをチェックするというサイクルを繰り返します。 しかし、コードが大きくなると、アプリ全体を手動でテストすることが難しくなります。

自動化されたテストでは、これらのテストを実行できます。 実際、ユニット テストは、アプリ コードが正しいことを確認するためのコードです。

ユニット テストが理解しにくいもう 1 つの理由は、Xcode でのテスト、アプリ アーキテクチャ、テスト ダブル、依存性注入など、多くのコンセプトがユニット テストを取り囲んでいることです。 プレーンな Swift で簡単なユニット テストを書くことができます。 特別な Xcode 機能や高度なテクニックは必要なく、Swift プレイグラウンド内でそれらを実行することさえ可能です。 以下の関数は、入力より小さいか等しいすべての正の整数の積である、整数の階乗を計算します。

1
2
3
4
5
6
7

func factorial(of number: Int) -> Int {
var result = 1
for factor in 1…number {
result = result * factor
} }..
return result
}

この関数を手動でテストするには、いくつかの数字を与えて、結果が期待通りかどうかチェックします。 たとえば、4 で試して、結果が 24 であることを確認します。

あなたのためにテストを行う Swift 関数を書いて、それを自動化することができます。 たとえば、次のようになります:

1
2
3
4
5

func testFactorial() {
if factorial(of: 4) != 24 {
print(“The factorial of 3 is wrong”)
}.
}

これは誰もが理解できるシンプルな Swift コードです。 関数の出力をチェックし、それが正しくない場合はメッセージを表示するだけです。

あるいは、少なくとも、私たちのアプリケーションのコードが、今書いた階乗関数のように単純であれば、それは事実でしょう。 そのため、Xcode のサポートとテスト ダブルのような高度なテクニックが必要になります。

それでも、このアイデアは単純なテストと複雑なテストの両方に役立ちます。

ユニット テストでは、到達しにくいエッジ ケースをチェックしてバグを防止できます

ユニット テストからいくつかのメリットを得ることができます。 アプリを実行し、テストしたい機能を持つ特定の場所に到達するのは、退屈なことです。 ユニット テストでは、実行中のアプリで作成するのが困難なエッジ ケースもテストできます。

たとえば、上記の factorial(of:) 関数にゼロまたは負の数を入力するとどうなるでしょうか。

1
2
3
4
5

func testFactorialOfZero() {
if factorial(of: 0) != 1 {
print(“The factorial of 0 is wrong”)
}.
}

Our code breaks:

実際のアプリでは、このコードはクラッシュを引き起こします。 ゼロは for ループの範囲外です。

しかし、私たちのコードは Swift の範囲の制限のために失敗するわけではありません。 それは、私たちのコードで非正整数を考慮することを忘れたので失敗します。 定義上、負の整数の階乗は存在せず、0 の階乗は 1 です。

1
2
3
4
5
6
7
8
9
10
11
12
13

func factorial(of number.X): Int) -> Int? {
if (number < 0) {
return nil
}.
if (number == 0) {
return 1
}.
var result = 1
for factor in 1…number {
result = result * factor
}
return result
}

これで再びテストを実行し、負の数に対するチェックも追加できるようになりました。

1
2
3
4
5

func testFactorialOfNegativeInteger() {
if factorial(of: -1) != nil {
print(“The factorial of -1 is wrong”)
}.
}

ここで、最後の、そして私の考えでは最も重要なユニットテストの利点を見ることができます。 私たちが以前に書いたテストは、factorial(of:) 関数を更新する際に、その既存のコードを壊さないことを確認します。

これは、継続的にコードを追加、更新、削除しなければならない複雑なアプリケーションでは非常に重要です。

Writing unit tests in Xcode

ユニット テストを理解した上で、次は Xcode のテスト機能を見ていきましょう。 例として、カロリーを記録するアプリを使用します。 完全な Xcode プロジェクトはここで見つけることができます。

このオプションをチェックすると、テンプレート プロジェクトにはすでに構成されたテスト ターゲットが含まれます。 後でいつでも追加できますが、私は通常、デフォルトでこのオプションをチェックします。 すぐにテストを書くつもりがなくても、テスト ターゲットは、テストのためのいくつかのテンプレート コードからすでに始まっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

import XCTest
@testable import Calories
class CaloriesTests.Tests: XCTestCase {
override func setUpWithError() throws {
// setup code をここに置く。 このメソッドは、クラス内の各テストメソッドの呼び出しの前に呼び出されます。
}
override func tearDownWithError() throws {
// ティアダウンコードをここに置く。 このメソッドは、クラス内の各テストメソッドの呼び出しの後に呼び出されます。
}
func testExample() throws {
// これは機能テストケースの例である。
// XCTAssert および関連する関数を使用して、テストが正しい結果を生成することを検証してください。
}
func testPerformanceExample() throws {
// これはパフォーマンステストケースの例です。
self.measure {
// ここに時間を計測したいコードを入れる。
}
}
}

  • @testable import Calories 行で、プロジェクトのメイン ターゲットのすべてのタイプにアクセスできます。 Swift モジュールのコードは、すべての型とメソッドが明示的に public であると宣言されない限り、外部からアクセスできません。 3060>
  • CaloriesTests クラスはテストケース、すなわち、関連するテストのグループ化を表します。 テストケース クラスは、XCTestCase から降下する必要があります。
  • setUpWithError() メソッドは、テスト ケース内のすべてのテストの標準環境をセットアップすることができます。 テストの後にいくつかのクリーニングを行う必要がある場合は、tearDownWithError() を使用します。 これらのメソッドは、テストケース内の各テストの前と後に実行されます。 私たちはそれらを必要としないので、それらを削除することができます。
  • testExample() メソッドは、私たちが階乗の例で書いたもののようなテストです。
  • そして最後に、testPerformanceExample() は、コードのパフォーマンスを測定する方法を示しています。 パフォーマンステストは、ユニットテストよりも標準的ではなく、高速である必要があるよりも重要なコードにのみ使用されます。

    あなたは、単に新しい .NET Framework を作成することによって、あなたのプロジェクトに新しいテストケースを追加することができます。

    優れたアーキテクチャはコードをテストしやすくする

    実際のプロジェクトでは、階乗の例のように緩い関数は通常存在しません。 その代わりに、構造体、列挙、およびアプリケーションのさまざまな部分を表すクラスがあります。

    MVCやMVVMなどのデザインパターンに従ってコードを整理することは非常に重要です。 よく設計されたコードを持つことは、コードをよく整理し、保守しやすくするだけではありません。

    これは、よくある質問に答える助けにもなります。 アプリケーション全体がそのコードの正しさに依存するため、わずかなテストでも大きな影響を及ぼします。

  • モデル タイプは多くの場合、構造体で、これは値タイプです。 これらは参照型よりもはるかにテストしやすいです (その理由は後ほど説明します)。
    参照型、つまりクラスには依存関係があり、副作用が発生します。 これらのユニット テストを記述するには、テスト ダブル (ダミー、スタブ、フェイク、スパイ、モック) が必要で、高度なテクニックを使用します。
  • Controller (MVC 用語) または View Model (MVVM 用語) は、しばしばディスク ストレージ、データベース、ネットワーク、デバイス センサーなどのリソースに接続されています。 また、並列コードを実行することもあります。 これらはユニットテストを難しくします。
  • ビューコードは最も頻繁に変更されるものであり、簡単に壊れるもろいテストを生成します。

Treating value types as pure functions to simplify unit testing

So, let’s add some model types to our app.では、アプリにいくつかのモデル型を追加してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

struct FoodItem {
let name.FoodItem {

Let name: String
let caloriesFor100Grams: Int
let grams: Int
}
struct Meal {
private(set) var items: =
var calories: Int {
var calories = 0
for item in items {
calories += (item.caloriesFor100Grams / 100) * item.grams
} }.
return calories
}.
mutating func add(_ item: FoodItem) {
items.append(item)
}.
}

Meal 構造体は、食品を追加し、それが含む食品の合計カロリーを計算するいくつかの簡単なロジックを含んでいます。 各ユニットは、ブラックボックスとして扱われます。

それは純粋な関数、すなわち、出力が入力にのみ依存し、いかなる副作用も生じない関数であったため、階乗の例では簡単でした。 それは、items 配列のコンテンツにのみ依存します。 add(_:) メソッドは、代わりに、値を返さず、食事の内部状態を変更するだけです。

しかし、構造体は値型であり、純粋な関数であると考えることができます。 依存関係を持たないので、構造体の初期状態をメソッドの入力、メソッド呼び出し後の状態を出力と考えることができます。

(これが、値型の中に参照型を入れることを控えるべき理由の1つです)。

XCTest フレームワークのアサーション機能を使用してコードを検証する

これで、テストするいくつかのコードと、Xcode プロジェクトでテストを記述する場所ができました。 それは、実際のユニットテストでは実用的ではありません。 どのテストが失敗したかを特定するために、ログをふるいにかけて時間を無駄にしたくありません。 XCTest フレームワークでは、合格しなかったテストを Xcode が指すようにする一連のアサーション関数を見つけることができます。

上で述べたように、ユニット テストはエッジケースをチェックするのに便利なので、空の食事にカロリーがないことを確認し始めましょう。

1
2
3
4
5
6

classes CaloriesTests.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.XXX XCTestCase {
func testEmptyMeal() throws {
let meal = Meal()
XCTAssertEqual(meal.calories, 0, “空の食事は 0 カロリーであるべき”)
}. ←クリック。
}

XCTestでは、任意の条件をアサーションできる汎用XCTAssert関数が提供されています。 あなたが書くどんなテストにもそれを使うことができますが、XCTAssertEqual(上記)、XCTAssertNil、および他のような、可能な場合より専門的なアサーション関数を使う方が良いです。 これらは XCTAssert よりも良いエラーを Xcode で生成します。

コード エディタのガターでその名前の横にあるダイヤモンドをクリックすることにより、単一のテストを実行できます。

テストが合格するとそれは緑になり、失敗テストは赤でマークされます。 テストの 0 を他の数字に変更して、失敗したテストがどのように見えるかを見ることができます。

クラス宣言の横にあるダイヤモンドをクリックすることによって、テストケースのすべてのテストを実行することができます。 キーボードで cmd+U を押すか、Xcode の [製品] メニューの [テスト] オプションを選択すると、すぐに実行できます。

Measuring the code coverage of a test in Xcode

ユニット テストに関する次の最も一般的な質問は、「コードのどの部分をテストすべきか」です。 すなわち、テストによってカバーされるコードの割合です。 そこで、その質問に答える前に、Xcode でテストのコード カバレッジをどのように測定するかを見てみましょう。 Xcode のツールバーのセグメント化されたコントロールのプロジェクト名のボタンをクリックします(停止ボタンの隣)。

そこで、アウトラインで[テスト]を選択し、[オプション]タブに移動します。 そして、最後に、[Gather coverage for] を選択します。 5736>

ここで、テストを再実行し、Xcode にカバレッジ データを収集させることができます。

ここで、プロジェクト全体と各ファイルのテストによってカバーされるコードのパーセンテージを確認できます。

代わりに、計算されたカロリー プロパティのカバレッジは 85.7% です。これは、テストがトリガーしなかったいくつかの実行パスがコード内にあることを意味します。 私たちは空の食事のカロリーだけをテストしたので、for ループのコードは実行されませんでした。

より高度な手法では、それほど単純ではないかもしれませんが、そのために、Xcode エディタでコード カバレッジ ストリップを表示できます。

これにより、コードの各行に対するカバレッジを示す帯が現れます。 赤色で、どの行がまったく実行されなかったか (フル)、または部分的にしか実行されなかったか (ストライプ) がわかります。

Why aiming for specific code coverage is meaningless and what you should do instead

では、良いコード カバー率は何パーセントなのでしょうか。 ある開発者は、20% で十分だと考えています。 また、70% や 80% といったより高い数字を目指す人もいます。 100%しか許容できないと考える開発者もいます。

私の意見では、コード カバレッジは意味のない指標です。 テストに関する決定を知らせるために使用することはできますが、達成しなければならない目標として扱うべきではありません。

その理由を知るために、まだテストしていないコードをカバーするために別のテストを書いてみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

class CaloriesTests: XCTestCase {
let banana = FoodItem(name: “Banana”, caloriesFor100Grams: 89, grams: 118)
let steak = FoodItem(name: “Steak”, caloriesFor100Grams: 271, grams.XCTestCase) = FoodItem(name: “Stea,”, caloriesFor100Grams: 119, grams: 225)
let goatCheese = FoodItem(name: “Goat Cheese”, caloriesFor100Grams: 364, grams: 28)
func testEmptyMeal() throws {
let meal = Meal()
XCTAssertEqual(meal.Test)
let goateCheese = FoodItem(name: “山羊チーズ”)calories, 0, “An empty meal should have 0 calories”)
}
func testCalories() {
var meal = Meal()
meal.add(banana)
meal.add(steak)
meal.add(goatCheese)
XCTAssertEqual(meal.items.count, 3)
XCTAssertEqual(meal.calories, 534)
} } {7799> {190} var meal = Meal()
meal.add(banana)
meal.add(steak) (goatCheese(バナナ))
var meal = Meal()
}

すべてのテストを再実行すると、Model.swiftファイルのカバレッジは100%になります。 良さそうでしょう?

さて、testEmptyMeal() メソッドを削除してきます。 残りのテストだけを実行すると、Meal 型のカバレッジはまだ 100% です。

このことから、100% という数字が間違った安心感を与えることがあることがわかります。 現在、calories computed プロパティのすべてのエッジケースをテストしていないことはご存知のとおりです。 しかし、テスト カバレッジはそれを反映していません。

さらに、カバレッジ 100% は誤解を招くだけでなく、有害でさえあることを付け加えます。 プロジェクト内の一部のコードは、特に最新のコードや、特にビューで、常に変更される傾向があります。

それを 100% カバーすることは、継続的に壊れ、修正しなければならない、もろいテストしか作成できないことを意味します。 これらのテストはバグを検出しません。

そして、ここで、誰も語らないポイントに到達します。

一部の開発者が主張するにもかかわらず、テストを書くことは楽しいことではありません。 テストを修正することは、さらに楽しくないことです。 それをやりすぎると、ユニットテストが嫌いになり、完全に脇に追いやられてしまいます。

心理学はベストプラクティスと同じくらいソフトウェア開発に不可欠です。 私たちは機械ではありません。

では、どのような数字を目指すべきなのでしょうか。 これには、より広範な議論が必要ですが、ここでは私の意見の要約を述べます。

プロジェクトの中で変更されることのない最も古いコードから始めて、有用なテストを書くこと。 コード カバレッジは、どのコードがまだテストされていないかを識別するためにのみ使用し、目標として使用したり、いつより多くのテストを書くかを決定したりしないようにします。

上で述べたように、モデル タイプには、プロジェクトで最も重要なコードが含まれており、また、最も変更されない傾向があります。 したがって、そこから始めましょう。

私は通常、すべての単一の実行パスをわざわざテストしません。 たとえば、実際のプロジェクトでは、私は上に示した testEmptyMeal() メソッドを書かないでしょう。

私は通常、testCalories() テストのみを記述します。 これは、私のコードが動作することを私に伝え、後でこのメソッドを変更するときにミスを犯した場合、私に警告を発します。 ユーザーを支援する機能のために実際のコードを書くことに時間を費やすことは、すべての実行パスをテストするよりも重要です。

この意見に対して一部の開発者から非難されることは分かっていますが、気にしないでください。 テスト カバレッジは、決して終わらない議論の 1 つです。

あなたが書くコードは、通常、ほとんど正しいです。 偏執的になる必要はありません。 あなたが何かを変更するたびに壊れるような大量のテストを持つことは、資産ではなく負債です。

あなたの時間と意志力は限られています。 MVC と MVVM による SwiftUI アプリのアーキテクチャ

いくつかのコードを一緒に投げれば、アプリを作ることは簡単です。 しかし、ベストプラクティスと堅牢なアーキテクチャがなければ、すぐに管理不可能なスパゲッティコードになってしまいます。 このガイドでは、SwiftUI アプリを適切に構造化する方法を紹介します。

今すぐ無料ブックを入手

コメントを残す

メールアドレスが公開されることはありません。