Xcode 6.3.2(Objective-C) + Quick + Nimble + KIF がうまく動かなかったハナシ
いつもはSpectaでテストをすることが多いのですが、
ミーハーな私は、巷でじわじわきているQuickを導入してみたが、うまくいかなかったので記します。
環境はこちら
- Xcode 6.3.2 (訳あってObjective-C)
- KIF (3.3.0):
- KIF/Core (= 3.3.0)
- KIF/Core (3.3.0)
- Nimble (0.4.2)
- Quick (0.3.1)
QuickとNimbleを用いてモデルのユニットテストを記述していた際にはうまく動作していたのですが、
いざUIテストの記述をしていた際におかしな動きになりました
なにが起こったか?
「KIFを使用してテストを行った際にきちんとエラーがキャッチされず、テストが失敗したことにならない」
なにが原因か?
そもそもKIFはテストのフレームワークに依存せずしない作りになっており、エラーの発生はフレームワーク側へ移譲する実装をしています。
If you want to use KIF with a test runner that does not subclass XCTestCase, your runner class just needs to implement the KIFTestActorDelegate protocol which contains two required methods.
(void)failWithException:(NSException *)exception stopTest:(BOOL)stop;
(void)failWithExceptions:(NSArray *)exceptions stopTest:(BOOL)stop;In the first case, the test runner should log the exception and halt the test execution if stop is YES. In the second, the runner should log all the exceptions and halt the test execution if stop is YES. The exceptions take advantage of KIF's extensions to NSException that include the lineNumber and filename in the exception's userData to record the error's origin.
https://github.com/kif-framework/KIF#use-with-other-testing-frameworks
つまり、Quick側の以下のメソッドの中身が怪しい
(void)failWithException:(NSException *)exception stopTest:(BOOL)stop; (void)failWithExceptions:(NSArray *)exceptions stopTest:(BOOL)stop;
さっそくQuickのソースコードを読んでみる。
/** This method is used to record failures, whether they represent example expectations that were not met, or exceptions raised during test setup and teardown. By default, the failure will be reported as an XCTest failure, and the example will be highlighted in Xcode. */ - (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected { if (self.example.isSharedExample) { filePath = self.example.callsite.file; lineNumber = self.example.callsite.line; } [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; }
テストを失敗させ、XCode上のファイルにハイライトをつける処理を実装している。
一見間違ってなさそうに見えるので、かなり混乱しましたが、
「recordFailureWithDescription:inFile:atLine:expected:」を呼び出すのはselfに対してではなく、現在実行されているテストのXCTestCaseのインスタンスというのが正解です。
使用しているv0.3.1より上のバージョンにコミットが積まれていました。
ただしこの修正が含まれているバージョンを使用するには、Xcode 7 / Swift 2.0 である必要があります。
つまり詰んだ...orz
解決法
カテゴリで拡張するしかない?
そもそも自分の使い方が悪いんじゃないか、導入の仕方がまずい?など色々悩みましたが、
Spectaでやっていた際には起きなかった事象なので無理やりカテゴリで解決しました。。
#import <Quick/QuickSpec.h> #import <Quick/Quick-Swift.h> #import <Quick/QuickConfiguration.h> #import <Quick/NSString+QCKSelectorName.h> #import <objc/runtime.h> @interface QuickSpec (WithObjC) @property (nonatomic, strong) XCTestRun *testRun; @property (nonatomic, strong) Example *example; @end @implementation QuickSpec (WithObjC) static QuickSpec *currentSpec = nil; @dynamic testRun, example; - (void)setTestRun:(XCTestRun *)testRun { objc_setAssociatedObject(self, _cmd, testRun, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (XCTestRun *)testRun { return objc_getAssociatedObject(self, @selector(setTestRun:)); } - (void)performTest:(XCTestRun *)run { self.testRun = run; [super performTest:run]; } + (SEL)addInstanceMethodForExample:(Example *)example { IMP implementation = imp_implementationWithBlock(^(QuickSpec *self){ currentSpec = self; [example run]; }); const char *types = [[NSString stringWithFormat:@"%s%s%s", @encode(id), @encode(id), @encode(SEL)] UTF8String]; SEL selector = NSSelectorFromString(example.name.qck_selectorName); class_addMethod(self, selector, implementation, types); return selector; } - (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected { if (self.example.isSharedExample) { filePath = self.example.callsite.file; lineNumber = self.example.callsite.line; } [currentSpec.testRun recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; } - (void)failWithException:(NSException *)exception stopTest:(BOOL)stop { self.continueAfterFailure = !stop; NSException *copyException = [exception copy]; [self recordFailureWithDescription:copyException.description inFile:copyException.userInfo[@"SenTestFilenameKey"] atLine:[copyException.userInfo[@"SenTestLineNumberKey"] integerValue] expected:YES]; } - (void)failWithExceptions:(NSArray *)exceptions stopTest:(BOOL)stop { NSException *lastException = exceptions.lastObject; for (NSException *exception in exceptions) { [self failWithException:exception stopTest:(exception == lastException ? stop : NO)]; } } @end
多分(間違いなく)もっといい方法がある気がしますが、
発生していたエラーは解決し、KIFでテストが失敗した際にもうまく動作するようになりました。
しばらくこれで様子をみます。
最適な解決法があればツッコミをお願いします。