Help us understand the problem. What is going on with this article?

【Objective-C】XCTestで非公開クラスの非公開メソッドをリフレクションを使ってテストする

More than 1 year has passed since last update.

この記事では、XCTestで非公開クラスの非公開メソッドをユニットテストする方法を紹介します。

クラス自体が非公開の場合、公開クラスをテストする時のように、テスト対象クラスのヘッダファイルをimportするやり方では記述できないので、代わりにリフレクションを使うことでメソッドのテストができます。

※そもそも設計が悪いのではないかとか、publicで提供するべきではないかといった議論はまた別の機会に。

テスト対象クラスの例

PublicClass.h/.mというファイルがあり、PublicClass.mにPrivateClassという今回のテスト対象クラスが実装されているものとします。

PublicClass.h
#import <Foundation/Foundation.h>

@interface PublicClass : NSObject

// Something

@end

PublicClass.m
#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引数以降は実際のテストするメソッドに渡す値を指定します。


以上で非公開クラスの非公開メソッドをテストすることができました。
非公開なモノにアクセスするのはけっこう面倒なので、まずは実装の見直しや本当にテストする必要があるのか、といったことから検討すると良いかもしれません。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした