Swiftのプライベートメソッドをユニットテストする方法

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20.Join Swift: How to Unit Test Private Methods in Swift
The Missing Manual
for Swift Development

The Guide I Wish I Had I Started Out

Join 20,Swift 開発について学ぶ 000 人以上の開発者たち

無料コピーをダウンロード

Swift プロジェクトのユニットテストは、Objective-C で書かれたプロジェクトのユニットテストとはかなり異なります。 Objective-C ランタイムの柔軟性に慣れている人にとっては、後ろ手に縛られているように感じるかもしれません。

アクセス制御

アクセス制御は多くの利点を持つ非常に歓迎すべき追加機能ですが、特にユニットテストに慣れていない場合は、ユニットテストを複雑にする場合があります。 内部として宣言されているエンティティにアクセスするために、テスト ターゲットで import ステートメントに testable 属性を適用できることはご存知でしょう。

import XCTest@testable import Notesclass NotesTests: XCTestCase { ...}

これは便利な追加機能ですが、テスト ターゲットのプライベートなエンティティにアクセスできるわけではありません。 ここで、今日の疑問が生まれました。 プライベート エンティティをどのようにユニット テストするのか。

間違った質問

この質問に対する短い答えは単純です。 他のモジュールからプライベートなエンティティにアクセスすることはできませんし、これはテスト ターゲットにも当てはまります。 これはテスト ターゲットにも当てはまります。 それはアクセス制御のためにあるものです。

しかし、それは質問に対する答えではありません。 プライベートなエンティティをどのようにユニットテストするかという質問であれば、それは間違った質問をしていることになります。 しかし、それはなぜでしょうか。

None of Your Business

なぜエンティティをプライベートと宣言するのでしょうか。 そうする動機は何でしょうか。 次の例を見てみましょう。

import Foundationstruct AccountViewViewModel { // MARK: - Properties let account: Account // MARK: - Public Interface var subscriptionAsString: String { switch account.subscription { case .monthly: return "Monthly Subscription" case .yearly: return "Yearly Subscription" case .trial: return "Trial Subscription" } } var expiresAtAsString: String { // Parse Date let date = parse(date: account.expiresAt) // Initialize Date Formatter let dateFormatter = DateFormatter() // Configure Date Formatter dateFormatter.dateFormat = "YYYY, MMMM" // Convert Date to String return dateFormatter.string(from: date) } // MARK: - Private Interface private func parse(date dateAsString: String) -> Date { let dateFormat: String if dateAsString.contains("/") { dateFormat = "YYYY'/'MM'/'dd" } else { dateFormat = "YYYYMMdd" } // Initialize Date Formatter let dateFormatter = DateFormatter() // Configure Date Formatter dateFormatter.dateFormat = dateFormat if let date = dateFormatter.date(from: dateAsString) { print(date) return date } else { fatalError("Incompatible Date Format") } }}

私は AccountViewViewModel 構造体をユニットテストしたいと思います。 ご覧のように、AccountViewViewModel 構造体は 2 つの内部計算プロパティを公開し、また、プライベート メソッドを定義しています。 expiresAtAsString 計算されるプロパティは、その作業の一部をプライベート parse(date:) メソッドにオフロードします。

// MARK: - Tests for Subscription as Stringfunc testSubscriptionAsString_Monthly() { let account = Account(expiresAt: "20161225", subscription: .monthly) let accountViewViewModel = AccountViewViewModel(account: account) XCTAssertEqual(accountViewViewModel.subscriptionAsString, "Monthly Subscription")}func testSubscriptionAsString_Yearly() { let account = Account(expiresAt: "20161225", subscription: .yearly) let accountViewViewModel = AccountViewViewModel(account: account) XCTAssertEqual(accountViewViewModel.subscriptionAsString, "Yearly Subscription")}func testSubscriptionAsString_Trial() { let account = Account(expiresAt: "20161225", subscription: .trial) let accountViewViewModel = AccountViewViewModel(account: account) XCTAssertEqual(accountViewViewModel.subscriptionAsString, "Trial Subscription")}// MARK: - Tests for Expires at as Stringfunc testExpiresAtAsString_20161225() { let account = Account(expiresAt: "20161225", subscription: .trial) let accountViewViewModel = AccountViewViewModel(account: account) XCTAssertEqual(accountViewViewModel.expiresAtAsString, "2016, December")}

しかし、プライベート メソッドをどのようにテストすればよいのでしょうか。 テスト ターゲットからプライベート メソッドにアクセスすることはできません。 しかし、なぜプライベート メソッドをユニット テストする必要があるのでしょうか。 私たちはそれを理由があってプライベートとしてマークしました。 そうでしょう? そして、それが私たちが始めた疑問への答えになります。

Unit Testing the Public Interface

AccountViewViewModel 構造体のパブリック インターフェイスをユニット テストすることにより、構造体のプライベート インターフェイスを自動的に、または暗黙のうちにユニット テストすることになるのです。 パブリック インターフェイスが徹底的にテストされていることを確認するタスクがあります。 つまり、AccountViewViewModel構造体のすべてのコードパスがユニットテストでカバーされていることを確認する必要があります。 言い換えれば、ユニットテストのスイートは、完全なコードカバレッジをもたらすべきです。

Xcode でコード カバレッジを有効にして、AccountViewViewModel 構造体のユニット テストを実行すると、いくつかのコード パスが実行されないことがわかります。 致命的なエラーのコード パスは無視できます。 私は致命的なエラーになるコード パスをユニット テストすることはありませんが、これはプロジェクトで致命的なエラーをどのように使用するかに大きく依存します。

1 つのユニット テストを追加することにより、AccountViewViewModel 構造体のコード カバー率を向上させることができます。

func testExpiresAtAsString_20161225WithForwardSlashes() { let account = Account(expiresAt: "2016/12/25", subscription: .trial) let accountViewViewModel = AccountViewViewModel(account: account) XCTAssertEqual(accountViewViewModel.expiresAtAsString, "2016, December")}

Code Coverage in Xcode

Implementation and Specification

AccountViewViewModel struct の仕様をテストしていることを理解することは重要です。 その実装をテストしているわけではありません。 これは似ているように聞こえるかもしれませんが、実際には非常に異なっています。 私たちは AccountViewViewModel 構造体の機能をテストしているのです。

この記事のキーポイントは、プライベートなエンティティはユニット テストする必要がないことです。 ユニット テストはブラックボックス テスティングの一種です。 これは、AccountViewViewModel 構造体の実装をテストせず、その仕様をテストすることを意味します。

ただし、実装に興味がないということではありません。 ユニット テストのスイートが、テストしているエンティティのすべてのコード パスをカバーしていることを確認する必要があります。 コード カバレッジ レポートは、これを達成するために非常に重要です。

for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy

コメントを残す

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