[Xcode, Objective-C] `SenTestingKit` でユニットテスト

Xcode に標準でついてる SenTestingKit.framework を利用して単体テストをするメモ。XCodeユニットテストガイドが公式のドキュメント。

プロジェクトにテストターゲットを追加

この手順はプロジェクト作成時にテストターゲットも一緒に作成した場合は必要がない。

  • File > New > Target… > iOS > Target > Cocoa Touch Unit Testing Bundle でテスト用のターゲットを追加
  • テストターゲットの Build Phases > Target Dependencies に本系のターゲットを追加する。
  • テストターゲットの Build Phases > Link Binary With LibrarySenTestingKit.framework を追加する。
  • テストターゲットの Build Phases > BuildSettingsBundle Loder$(BUILT_PRODUCTS_DIR)/アプリ名.app/アプリ名 を追加する (ロジックテストの場合は不要)
  • テストターゲットの Build Phases > BuildSettingsTest Host$(BUNDLE_LOADER) を追加する (ロジックテストの場合は不要)

※参考 : "Include Unit Tests"をチェックし忘れたプロジェクトにUnitTestの設定をする - おかひろの雑記

※本系に新しいクラスを作るときターゲットを選択する項目があるけど、テストターゲットを含める必要はない。テスト用に作成したクラスのみテストターゲットの対象にする。

cocos2dだとメンドくさそう…(cocos2dSenTestingKitをつかった`UnitTestを行う場合の注意点とか - haoyayoi Dev Style - iPhoneアプリ開発グループ)

テキトーにクラスを作成してテスト

以下はテキトーです。

本系ターゲット用にテキトーなクラスを準備

Serivce.h ヘッダー

#import <Foundation/Foundation.h>

@interface Service : NSObject
- (void)publicMethod;
@end

Service.m 実装

#import "Service.h"

@interface Service ()
- (void)privateMethod;
@end

@implementation Service

- (void)publicMethod {
  printf("Service#publicMethod \n");
}

- (void)privateMethod {
  printf("Service#privateMethod \n");
}

@end

ServiceLocator.h ヘッダー

#import <Foundation/Foundation.h>
#import "Service.h"

@interface ServiceLocator : NSObject
+ (void)setInstance:(Service *)instance;
+ (Service *)getInstance;
@end

ServiceLocator.m 実装

#import "ServiceLocator.h"

static Service *_instance = nil;

@implementation ServiceLocator

+ (void)setInstance:(Service *)instance {
  _instance = instance;
}

+ (Service *)getInstance {
  return _instance;
}

@end

テスト系ターゲット用にテキトーなクラスを用意

Service を継承した ServiceForTest.h ヘッダー

#import "Service.h"

// テストでは隠蔽していたメソッドにアクセスしたい
// ヘッダーで再定義することでアクセスできるようにする…
// (別に再定義しなくてもアクセスできるけど警告が出る)
// ※これ以外の方法もある
//
// 参考: http://www.tokoro.me/2012/09/12/objc-private-test/
// 参考: http://d.hatena.ne.jp/dkfj/20120909/1347176787
@interface Service ()
- (void)privateMethod;
@end

@interface ServiceForTest : Service // テスト用に処理を切り替える場合に継承して、その部分の処理を変更する。
@end

Service を継承した ServiceForTest.h 実装

#import "ServiceForTest.h"

@implementation ServiceForTest

- (void)publicMethod {
  printf("ServiceForTest#publicMethod \n");
}

- (void)privateMethod {
  printf("ServiceForTest#privateMethod \n");
}

@end

テスト系のターゲットにテストクラスを追加

ヘッダ内容は省略。SenTest 継承してるだけ。

#import "テストクラス.h"
#import "ServiceLocator.h"
#import "ServiceForTest.h"

@implementation テストクラス

// テスト前に呼ばれる SenTest#setUp メソッド
- (void)setUp {
  [super setUp];

  // Set-up code here.

  // テスト時はテスト用のインスタンスを使用する
  [ServiceLocator setInstance:[[ServiceForTest alloc] init]];
}

// テスト後に呼ばれる SenTest#tearDown メソッド
- (void)tearDown
{
  // Tear-down code here.

  [super tearDown];
}

// testプレフィックスのついたメソッドがテスト実行される
// testから始まらないメソッドでもアサーションは可能だけど
// SenTestingフレームワークからは直接呼ばれない。
- (void)testExample {
  Service *instance = [ServiceLocator getInstance];
  [instance publicMethod];
  [instance privateMethod];

  STFail(@"STAssert**** などでアサーションを行う");
}

@end

それぞれのメソッドが実行される前後で SenTest#setup, SenTest#tearDown が実行される。 それぞれのテストメソッドは並列実行されずスレッドセーフとなっている。 実行される順番はメソッド名でソートされた順番が使用されるみたいだ。

以下、アサーション用のマクロ達

- (void)testFail {
  STFail(@"テストを失敗させる"); // 失敗!
}

- (void)testAssertEqualObjects {
  STAssertEqualObjects(@"テスト",
                      @"テスト",
                       @"isEqualsTo: メソッドがNOなら失敗");
}

- (void)testAssertEquals {
  STAssertEquals(CGRectMake(1, 2, 3, 4),
                 CGRectMake(1, 2, 3, 4),
                 @"スカラ, 構造体, 共用体の比較でNOなら失敗");
}

- (void)testAssertEqualsWithAccuracy {
  STAssertEqualsWithAccuracy(3.0f,
                             2.0f,
                             1L,
                             @"数値の差が精度を割ると失敗");
  STAssertEqualsWithAccuracy(3L,
                             2,
                             1,
                             @"型が異なると失敗"); // Type mismatch(失敗)
}

- (void)testAssertNilOrNotNil {
  STAssertNil(nil,
              @"nilでなければ失敗");
  STAssertNotNil([NSArray array],
                 @"nilなら失敗");
}

- (void)testAssertBool {
  STAssertTrue(YES,
               @"NOなら失敗");
  STAssertFalse(NO,
                @"YESなら失敗");

  STAssertTrueNoThrow(YES,
                      @"YESかつ例外が投げられないなら成功");
  STAssertFalseNoThrow(NO,
                       @"NOかつ例外が投げられないなら成功");
}

- (void)testAssertThrows {
  NSArray *array = [NSArray arrayWithObjects:nil];
  STAssertThrows([array objectAtIndex:100],
                 @"例外が投げられないと失敗");
  STAssertNoThrow([array objectAtIndex:100],
                  @"例外が投げられると失敗"); // 失敗!
}

メモ

  • SenTest サブクラスをテストスイート(TestSuite)と呼ぶ。
  • ネガティブテスト
  • バグ修正をテストケースでカバーする
Share
関連記事