ユニットテストを理解するのに本当に苦労しています。 TDDの重要性は理解していますが、私が読んだ単体テストの例はすべて非常に単純で些細なもののようです。たとえば、プロパティが設定されていること、またはメモリが配列に割り当てられていることを確認するためのテスト。どうして?コード化した場合..alloc] init]
、本当に動作することを確認する必要がありますか?
私は開発に不慣れなので、TDDを取り巻くすべての流行で、ここで何かを見逃していると確信しています。
私の主な問題は、実用的な例が見つからないことだと思います。テストに適した候補と思われるメソッドsetReminderId
を次に示します。これが機能していることを確認するには、便利な単体テストはどのように見えますか? (OCUnitを使用)
- (NSNumber *)setReminderId: (NSDictionary *)reminderData
{
NSNumber *currentReminderId = [[NSUserDefaults standardUserDefaults] objectForKey:@"currentReminderId"];
if (currentReminderId) {
// Increment the last reminderId
currentReminderId = @(currentReminderId.intValue + 1);
}
else {
// Set to 0 if it doesn't already exist
currentReminderId = @0;
}
// Update currentReminderId to model
[[NSUserDefaults standardUserDefaults] setObject:currentReminderId forKey:@"currentReminderId"];
return currentReminderId;
}
更新:この答えを2つの点で改善しました。これはスクリーンキャストになり、プロパティインジェクションからコンストラクタインジェクションに切り替えました。 Objective-C TDDの使用を開始する方法を参照してください
トリッキーな部分は、メソッドが外部オブジェクトNSUserDefaultsに依存していることです。 NSUserDefaultsを直接使用する必要はありません。代わりに、この依存関係を何らかの形で注入する必要があります。これにより、偽のユーザーのデフォルトをテスト用に置き換えることができます。
これにはいくつかの方法があります。 1つは、メソッドに追加の引数として渡すことです。もう1つは、それをクラスのインスタンス変数にすることです。そして、このivarを設定するにはさまざまな方法があります。イニシャライザ引数で指定される「コンストラクタインジェクション」があります。または、「プロパティインジェクション」があります。 iOS SDKの標準オブジェクトの場合、私の好みは、デフォルト値を持つプロパティにすることです。
それでは、プロパティがデフォルトでNSUserDefaultsであることのテストから始めましょう。ちなみに、私のツールセットはXcodeの組み込みOCUnitです。アサーションの場合は OCHamcrest 、モックオブジェクトの場合は OCMockito です。他にも選択肢はありますが、それを使用しています。
より適切な名前がない場合、クラスにはExample
という名前が付けられます。インスタンスは、「テスト中のシステム」のsut
という名前になります。プロパティの名前はuserDefaults
になります。 ExampleTests.mで、デフォルト値を確認する最初のテストを次に示します。
#import <SenTestingKit/SenTestingKit.h>
#define HC_SHORTHAND
#import <OCHamcrestIOS/OCHamcrestIOS.h>
@interface ExampleTests : SenTestCase
@end
@implementation ExampleTests
- (void)testDefaultUserDefaultsShouldBeSet
{
Example *sut = [[Example alloc] init];
assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class])));
}
@end
この段階では、これはコンパイルされません—テストの失敗としてカウントされます。見てください。角かっことかっこをスキップして目を離せれば、テストはかなり明確になるはずです。
そのテストをコンパイルして実行し、失敗するようにできる最も簡単なコードを書いてみましょう。 Example.hは次のとおりです。
#import <Foundation/Foundation.h>
@interface Example : NSObject
@property (strong, nonatomic) NSUserDefaults *userDefaults;
@end
そして畏敬の念を起こさせるExample.m:
#import "Example.h"
@implementation Example
@end
ExampleTests.mの最初に行を追加する必要があります。
#import "Example.h"
テストが実行され、「NSUserDefaultsのインスタンスが必要ですが、nilでした」というメッセージで失敗します。まさに私たちが欲しかったもの。最初のテストのステップ1に達しました。
ステップ2は、そのテストに合格するための最も単純なコードを記述することです。これはどう:
- (id)init
{
self = [super init];
if (self)
_userDefaults = [NSUserDefaults standardUserDefaults];
return self;
}
合格!ステップ2が完了しました。
ステップ3は、すべての変更を本番コードとテストコードの両方に組み込むためにコードをリファクタリングすることです。しかし、まだクリーンアップするものはまだ何もありません。最初のテストが完了しました。これまでに何がありますか? NSUserDefaults
にアクセスできるが、テスト用にオーバーライドされるクラスの始まり。
次に、メソッドのテストを記述しましょう。何をしたいですか?ユーザーのデフォルトに一致するキーがない場合は、0を返します。
モックオブジェクトを最初に使用するときは、最初に手作業で作成することをお勧めします。次に、モックオブジェクトフレームワークの使用を開始します。しかし、先にジャンプしてOCMockitoを使用して物事を高速化します。次の行をExampleTest.mに追加します。
#define MOCKITO_SHORTHAND
#import <OCMockitoIOS/OCMockitoIOS.h>
デフォルトでは、OCMockitoベースのモックオブジェクトは、すべてのメソッドに対してnil
を返します。しかし、「objectForKey:@"currentReminderId"
を要求すると、nil
が返されます」と言って、期待を明確にするための追加のコードを記述します。そして、これらすべてを考慮して、メソッドがNSNumber 0を返すようにしたいと思います(引数の意味がわからないので、引数を渡しません。また、メソッドにnextReminderId
という名前を付けます)。
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
Example *sut = [[Example alloc] init];
NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil];
assertThat([sut nextReminderId], is(equalTo(@0)));
}
これはまだコンパイルされていません。 Example.hでnextReminderId
メソッドを定義してみましょう。
- (NSNumber *)nextReminderId;
そして、これがExample.mの最初の実装です。テストを失敗させたいので、偽の数を返します。
- (NSNumber *)nextReminderId
{
return @-1;
}
テストは失敗し、「予期された<0>でしたが<-1>でした」というメッセージが表示されます。これはテストをテストする私たちの方法であるため、テストが失敗することは重要です。また、コードを記述して、テストを失敗状態から合格状態に切り替えます。ステップ1が完了しました。
ステップ2:テストテストに合格します。ただし、テストに合格する最も単純なコードが必要であることを忘れないでください。それはひどくばかげて見えるでしょう。
- (NSNumber *)nextReminderId
{
return @0;
}
すごい、合格!ただし、このテストはまだ完了していません。次に、ステップ3:リファクタリングに進みます。テストに重複したコードがあります。テスト対象のシステムであるsut
をivarに取り込みましょう。 -setUp
メソッドを使用して設定し、-tearDown
を使用してクリーンアップ(破棄)します。
@interface ExampleTests : SenTestCase
{
Example *sut;
}
@end
@implementation ExampleTests
- (void)setUp
{
[super setUp];
sut = [[Example alloc] init];
}
- (void)tearDown
{
sut = nil;
[super tearDown];
}
- (void)testDefaultUserDefaultsShouldBeSet
{
assertThat([sut userDefaults], is(instanceOf([NSUserDefaults class])));
}
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil];
assertThat([sut nextReminderId], is(equalTo(@0)));
}
@end
テストが再度実行され、テストが成功することを確認します。リファクタリングは、「緑」または合格の状態でのみ行う必要があります。リファクタリングがテストコードで行われたか、本番コードで行われたかに関係なく、すべてのテストは引き続き成功するはずです。
次に、別の要件をテストします。ユーザーのデフォルトを保存する必要があります。前のテストと同じ条件を使用します。ただし、既存のテストにアサーションを追加するのではなく、新しいテストを作成します。理想的には、各テストで1つのことを検証し、一致する適切な名前を付ける必要があります。
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults
{
// given
NSUserDefaults *mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:nil];
// when
[sut nextReminderId];
// then
[verify(mockUserDefaults) setObject:@0 forKey:@"currentReminderId"];
}
verify
ステートメントは、OCMockitoで「このモックオブジェクトはこの方法で一度呼び出されるべきだった」という言い方です。テストを実行すると、「1回の一致呼び出しが予期されていますが、0回のエラーが発生しました」というエラーが発生します。ステップ1が完了しました。
ステップ2:通過する最も単純なコード。準備はいい?ここに行く:
- (NSNumber *)nextReminderId
{
[_userDefaults setObject:@0 forKey:@"currentReminderId"];
return @0;
}
「しかし、なぜその値を持つ変数ではなく、ユーザーのデフォルトで@0
を保存するのですか?」あなたが尋ねる。それは、私たちがテストした限りです。少々お待ちください。
ステップ3:リファクタリング。ここでも、テストに重複したコードがあります。 mockUserDefaults
をivarとして引き出しましょう。
@interface ExampleTests : SenTestCase
{
Example *sut;
NSUserDefaults *mockUserDefaults;
}
@end
テストコードは、「 'mockUserDefaults'のローカル宣言によりインスタンス変数を非表示にします」という警告を表示します。 ivarを使用するように修正します。次に、ヘルパーメソッドを抽出して、各テストの開始時にユーザーデフォルトの条件を確立します。 nil
を別の変数に取り出して、リファクタリングに役立てましょう。
NSNumber *current = nil;
mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:current];
最後の3行を選択し、コンテキストクリックして、[リファクタリング]→[抽出]を選択します。 setUpUserDefaultsWithCurrentReminderId:
という新しいメソッドを作成します
- (void)setUpUserDefaultsWithCurrentReminderId:(NSNumber *)current
{
mockUserDefaults = mock([NSUserDefaults class]);
[sut setUserDefaults:mockUserDefaults];
[given([mockUserDefaults objectForKey:@"currentReminderId"]) willReturn:current];
}
これを呼び出すテストコードは次のようになります。
NSNumber *current = nil;
[self setUpUserDefaultsWithCurrentReminderId:current];
この変数の唯一の理由は、自動リファクタリングを支援することでした。それをインライン化しましょう:
[self setUpUserDefaultsWithCurrentReminderId:nil];
テストはまだ成功しています。 Xcodeの自動リファクタリングでは、そのコードのすべてのインスタンスが新しいヘルパーメソッドの呼び出しに置き換えられなかったため、自分で行う必要があります。したがって、テストは次のようになります。
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldReturnZero
{
[self setUpUserDefaultsWithCurrentReminderId:nil];
assertThat([sut nextReminderId], is(equalTo(@0)));
}
- (void)testNextReminderIdWithNoCurrentReminderIdInUserDefaultsShouldSaveZeroInUserDefaults
{
// given
[self setUpUserDefaultsWithCurrentReminderId:nil];
// when
[sut nextReminderId];
// then
[verify(mockUserDefaults) setObject:@0 forKey:@"currentReminderId"];
}
移動しながら継続的に掃除する方法をご覧ください。テストが実際に読みやすくなりました!
ここで、ユーザーのデフォルトに何らかの値がある場合、1つ大きい値を返すことをテストします。任意の値3を使用して、「ゼロを返す必要がある」テストをコピーして変更します。
- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldReturnOneGreater
{
[self setUpUserDefaultsWithCurrentReminderId:@3];
assertThat([sut nextReminderId], is(equalTo(@4)));
}
これは、必要に応じて失敗します。「<4>が必要ですが、<0>でした」。
テストに合格するための簡単なコードは次のとおりです。
- (NSNumber *)nextReminderId
{
NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"];
if (reminderId)
reminderId = @([reminderId integerValue] + 1);
else
reminderId = @0;
[_userDefaults setObject:@0 forKey:@"currentReminderId"];
return reminderId;
}
そのsetObject:@0
を除いて、これはあなたの例のように見え始めています。リファクタリングするものはまだありません。 (実際はありますが、後で気がついたので、続けましょう。)
これで、もう1つのテストを確立できます。同じ条件が与えられた場合、新しいリマインダーIDがユーザーのデフォルトに保存されます。これは、以前のテストをコピーして変更し、適切な名前を付けることですばやく実行できます。
- (void)testNextReminderIdWithCurrentReminderIdInUserDefaultsShouldSaveOneGreaterInUserDefaults
{
// given
[self setUpUserDefaultsWithCurrentReminderId:@3];
// when
[sut nextReminderId];
// then
[verify(mockUserDefaults) setObject:@4 forKey:@"currentReminderId"];
}
そのテストは失敗し、「1回の一致呼び出しが期待されますが、0を受け取りました。もちろん、パスを取得するには、setObject:@0
をsetObject:reminderId
に変更するだけです。すべてが通過します。終わったね!
待って、私たちは終わっていません。ステップ3:リファクタリングするものはありますか?私がこれを最初に書いたとき、私は「本当ではない」と言った。しかし、見た後にそれを見直す クリーンコードエピソード 、ボブおじさんが「関数はどれくらい大きくすべきか?4行でよい、おそらく5行でよい。6行は大丈夫です。10行はそうです。大きすぎる。"それは7行です。私は何を取りこぼしたか?それは、複数のことを行うことによって、関数の規則に違反しているに違いありません。
ボブおじさん:「関数が1つのことを確実に行う唯一の方法は、ドロップするまで抽出することです。」これらの最初の4行は一緒に機能します。実際の値を計算します。それらを選択して、リファクタリング▶抽出します。エピソード2からのボブおじさんのスコープルールに従って、使用範囲が非常に限られているため、ニースの長くわかりやすい名前を付けます。自動リファクタリングによって得られるものは次のとおりです。
- (NSNumber *)determineNextReminderIdFromUserDefaults
{
NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"];
if (reminderId)
reminderId = @([reminderId integerValue] + 1);
else
reminderId = @0;
return reminderId;
}
- (NSNumber *)nextReminderId
{
NSNumber *reminderId;
reminderId = [self determineNextReminderIdFromUserDefaults];
[_userDefaults setObject:reminderId forKey:@"currentReminderId"];
return reminderId;
}
よりきれいにするためにそれをきれいにしましょう:
- (NSNumber *)determineNextReminderIdFromUserDefaults
{
NSNumber *reminderId = [_userDefaults objectForKey:@"currentReminderId"];
if (reminderId)
return @([reminderId integerValue] + 1);
else
return @0;
}
- (NSNumber *)nextReminderId
{
NSNumber *reminderId = [self determineNextReminderIdFromUserDefaults];
[_userDefaults setObject:reminderId forKey:@"currentReminderId"];
return reminderId;
}
これで、各メソッドは非常にタイトになり、メインメソッドの3行を読んで、それが何をするかを簡単に確認できます。しかし、私はそのユーザーのデフォルトのキーが2つの方法に分散していることに不快です。それをExample.mの先頭にある定数に抽出してみましょう。
static NSString *const currentReminderIdKey = @"currentReminderId";
その定数は、そのキーが製品コードに現れるところならどこでも使用します。ただし、テストコードは引き続きリテラルを使用します。これにより、誰かがその定数キーを誤って変更してしまうのを防ぎます。
だからあなたはそれを持っています。 5つのテストで、私はあなたが要求したコードにTDDしました。うまくいけば、TDDの方法と、それが価値がある理由が明確にわかります。 3ステップワルツに従うことによって
同じ場所にいるだけではありません。あなたは次のようになります:
これらすべてのメリットにより、TDDに費やす時間よりも多くの時間を節約できます。長期的にだけでなく、すぐに節約できます。
完全なアプリを含む例については、本Test-Driven iOS Developmentを入手してください。これが 私の本のレビュー です。