この記事では、XCTestで非公開クラスの非公開メソッドをユニットテストする方法を紹介します。
クラス自体が非公開の場合、公開クラスをテストする時のように、テスト対象クラスのヘッダファイルをimportするやり方では記述できないので、代わりにリフレクションを使うことでメソッドのテストができます。
※そもそも設計が悪いのではないかとか、publicで提供するべきではないかといった議論はまた別の機会に。
テスト対象クラスの例
PublicClass.h/.mというファイルがあり、PublicClass.mにPrivateClassという今回のテスト対象クラスが実装されているものとします。
#import <Foundation/Foundation.h>
@interface PublicClass : NSObject
// Something
@end
#import "PublicClass.h"
#pragma mark - PrivateClass
@interface PrivateClass : NSObject
@end
@implementation PrivateClass
#pragma mark - private method
- (NSInteger)privateMethodWithValue1:(NSInteger)value1 value2:(NSInteger)value2 {
return value1 + value2;
}
#pragma mark - private class method
+ (NSInteger)privateClassMethodWithValue1:(NSInteger)value1 value2:(NSInteger)value2 {
return value1 * value2;
}
@end
#pragma mark - PublicClass
@implementation PublicClass
// Something
@end
PrivateClassには、privateMethodWithValue1:value2:
というprivateなインスタンスメソッドとprivateClassMethodWithValue1:value2:
というprivateなクラスメソッドが実装されています。メソッドの内容は単純なので説明するまでもないでしょう。
この2つのメソッドが今回のテスト対象です。
XCTestでテストする
テストプログラムは以下の通りです。メソッドが返す計算結果が期待する値になっているかをテストしています。
リフレクションのやり方ですが、言葉で説明するより実際にソースコードを見てもらった方が早いでしょう。
#import <XCTest/XCTest.h>
typedef NSInteger (*PrivateInstanceMethod)(id, SEL, NSInteger, NSInteger);
typedef NSInteger (*PrivateClassMethod)(id, SEL, NSInteger, NSInteger);
@interface XCTestSampleTests : XCTestCase
@end
@implementation XCTestSampleTests
- (void)setUp {
[super setUp];
}
- (void)tearDown {
[super tearDown];
}
// インスタンスメソッドのテスト
- (void)testPrivateMethod {
Class cls = NSClassFromString(@"PrivateClass");
SEL sel = NSSelectorFromString(@"privateMethodWithValue1:value2:");
IMP method = [cls instanceMethodForSelector:sel];
PrivateInstanceMethod func = (void *) method;
id obj = [cls new];
NSInteger res = func(obj, sel, 2, 3);
XCTAssertEqual(res, 5);
}
// クラスメソッドのテスト
- (void)testPrivateClassMethod {
Class cls = NSClassFromString(@"PrivateClass");
SEL sel = NSSelectorFromString(@"privateClassMethodWithValue1:value2:");
IMP method = [cls methodForSelector:sel];
PrivateClassMethod func = (void *) method;
NSInteger res = func(cls, sel, 2, 3);
XCTAssertEqual(res, 6);
}
@end
ポイント解説
NSClassFromString
関数に非公開クラスのクラス名を指定してClassを作ります。
NSSelectorFromString
関数には非公開のメソッド名を指定してセレクタを作ります。(引数がある時は:
が必要ですので忘れずに)
次にセレクタからIMP
を作りますが、インスタンスメソッドの場合はinstanceMethodForSelector
メソッドを使い、クラスメソッドの場合はmethodForSelector
メソッドを使います。
typedefでテストするメソッドのカタチを定義していますが、第1引数のidと第2引数のSELは固定で、第3引数以降は実際のテストするメソッドの引数の型を列挙していきます。(引数の無いメソッドの場合は第2引数のSELまで)
func
には、インスタンスメソッドの場合は[cls new]
でインスタンスを生成して第1引数に、クラスメソッドの場合はcls
をそのまま第1引数に渡します。第2引数にはセレクタを指定します。第3引数以降は実際のテストするメソッドに渡す値を指定します。
以上で非公開クラスの非公開メソッドをテストすることができました。
非公開なモノにアクセスするのはけっこう面倒なので、まずは実装の見直しや本当にテストする必要があるのか、といったことから検討すると良いかもしれません。