-
Notifications
You must be signed in to change notification settings - Fork 52
Added a hook on SLTest exposing failures that are encountered during a test run. #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
3cb98a2
c1196aa
3d69830
70847f8
df27854
d39a6a3
625ed53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| // | ||
| // SLTestState.h | ||
| // Subliminal | ||
| // | ||
| // Created by Jacob Relkin on 8/22/14. | ||
| // Copyright (c) 2014 Inkling. All rights reserved. | ||
| // | ||
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| @class SLTestFailure; | ||
|
|
||
| /** | ||
| SLTestState objects define the state of a Subliminal test or test case. | ||
| */ | ||
|
|
||
| @interface SLTestState : NSObject | ||
|
|
||
| /** | ||
| Denotes whether the test or test case failed | ||
| */ | ||
| @property (nonatomic, readonly) BOOL failed; | ||
|
|
||
| /** | ||
| If the test or test case failure was expected. | ||
| */ | ||
| @property (nonatomic, readonly) BOOL failureWasExpected; | ||
|
|
||
| /** | ||
| Sets the properties above, from the failure's description. | ||
| The value of `failureWasExpected` is determined by the first failure that was encountered. | ||
|
|
||
| @param failure The test failure to record. | ||
| */ | ||
| - (void)recordFailure:(SLTestFailure *)failure; | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // | ||
| // SLTestState.m | ||
| // Subliminal | ||
| // | ||
| // Created by Jacob Relkin on 8/22/14. | ||
| // Copyright (c) 2014 Inkling. All rights reserved. | ||
| // | ||
|
|
||
| #import "SLTestState.h" | ||
| #import "SLTestFailure.h" | ||
|
|
||
| @interface SLTestState () | ||
|
|
||
| @property (nonatomic, readwrite) BOOL failed; | ||
| @property (nonatomic, readwrite) BOOL failureWasExpected; | ||
|
|
||
| @end | ||
|
|
||
| @implementation SLTestState | ||
|
|
||
| - (void)recordFailure:(SLTestFailure *)failure { | ||
| NSParameterAssert(failure); | ||
|
|
||
| if (!self.failed) { | ||
| self.failureWasExpected = failure.isExpected; | ||
| } | ||
|
|
||
| self.failed = YES; | ||
| } | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,14 +29,15 @@ | |
| #import <objc/runtime.h> | ||
| #import <objc/message.h> | ||
|
|
||
| #import "SLTestFailure.h" | ||
| #import "SLTestState.h" | ||
|
|
||
| // All exceptions thrown by SLTest must have names beginning with this prefix | ||
| // so that `-[SLTest exceptionByAddingFileInfo:]` can determine whether to attach | ||
| // call site information to exceptions. | ||
| static NSString *const SLTestExceptionNamePrefix = @"SLTest"; | ||
|
|
||
|
|
||
|
|
||
| @implementation SLTest | ||
|
|
||
| static NSString *__lastKnownFilename; | ||
|
|
@@ -330,26 +331,38 @@ + (NSString *)unfocusedTestCaseName:(NSString *)testCase { | |
| return testCase; | ||
| } | ||
|
|
||
| - (void)reportFailureInPhase:(SLTestFailurePhase)phase toState:(SLTestState *)state exception:(NSException *)exception testCaseSelector:(SEL)testCaseSelector { | ||
| SLTestFailure *failure = [SLTestFailure failureWithException:exception phase:phase testCaseSelector:testCaseSelector]; | ||
| NSException *exceptionToLog = [self exceptionByAddingFileInfo:exception]; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that this should be the exception that's recorded in the failure, as the most complete record of the event. |
||
| [[SLLogger sharedLogger] logException:exceptionToLog | ||
| expected:[failure isExpected]]; | ||
| [state recordFailure:failure]; | ||
| [self testDidEncounterFailure:failure]; | ||
| } | ||
|
|
||
| - (BOOL)runAndReportNumExecuted:(NSUInteger *)numCasesExecuted | ||
| failed:(NSUInteger *)numCasesFailed | ||
| failedUnexpectedly:(NSUInteger *)numCasesFailedUnexpectedly { | ||
| NSUInteger numberOfCasesExecuted = 0, numberOfCasesFailed = 0, numberOfCasesFailedUnexpectedly = 0; | ||
| SLTestState *testState = [SLTestState new]; | ||
|
|
||
| BOOL testDidFailInSetUpOrTearDown = NO; | ||
| @try { | ||
| [self setUpTest]; | ||
| } | ||
| @catch (NSException *exception) { | ||
| [[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception] | ||
| expected:[[self class] exceptionWasExpected:exception]]; | ||
| testDidFailInSetUpOrTearDown = YES; | ||
| [self reportFailureInPhase:SLTestFailurePhaseTestSetup | ||
| toState:testState | ||
| exception:exception | ||
| testCaseSelector:NULL]; | ||
| } | ||
|
|
||
| // if setUpTest failed, skip the test cases | ||
| if (!testDidFailInSetUpOrTearDown) { | ||
| if (!testState.failed) { | ||
| NSString *test = NSStringFromClass([self class]); | ||
| for (NSString *testCaseName in [[self class] testCasesToRun]) { | ||
| @autoreleasepool { | ||
| SLTestState *testCaseState = [SLTestState new]; | ||
|
|
||
| // all logs below use the focused name, so that the logs are consistent | ||
| // with what's actually running | ||
| [[SLLogger sharedLogger] logTest:test caseStart:testCaseName]; | ||
|
|
@@ -362,29 +375,29 @@ - (BOOL)runAndReportNumExecuted:(NSUInteger *)numCasesExecuted | |
| // (though we can't guarantee it won't be reused within a test case) | ||
| [SLTest clearLastKnownCallSite]; | ||
|
|
||
| BOOL caseFailed = NO, failureWasExpected = NO; | ||
| @try { | ||
| [self setUpTestCaseWithSelector:unfocusedTestCaseSelector]; | ||
| } | ||
| @catch (NSException *exception) { | ||
| caseFailed = YES; | ||
| failureWasExpected = [[self class] exceptionWasExpected:exception]; | ||
| [[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception] | ||
| expected:failureWasExpected]; | ||
| [self reportFailureInPhase:SLTestFailurePhaseTestCaseSetup | ||
| toState:testCaseState | ||
| exception:exception | ||
| testCaseSelector:unfocusedTestCaseSelector]; | ||
| } | ||
|
|
||
| // Only execute the test case if set-up succeeded. | ||
| if (!caseFailed) { | ||
| if (!testCaseState.failed) { | ||
| @try { | ||
| // We use objc_msgSend so that Clang won't complain about performSelector leaks | ||
| // Make sure to send the actual test case selector | ||
| ((void(*)(id, SEL))objc_msgSend)(self, NSSelectorFromString(testCaseName)); | ||
| } | ||
| @catch (NSException *exception) { | ||
| caseFailed = YES; | ||
| failureWasExpected = [[self class] exceptionWasExpected:exception]; | ||
| [[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception] | ||
| expected:failureWasExpected]; | ||
| [self reportFailureInPhase:SLTestFailurePhaseTestCaseExecution | ||
| toState:testCaseState | ||
| exception:exception | ||
| testCaseSelector:unfocusedTestCaseSelector]; | ||
|
|
||
| } | ||
| } | ||
|
|
||
|
|
@@ -394,22 +407,22 @@ - (BOOL)runAndReportNumExecuted:(NSUInteger *)numCasesExecuted | |
| [self tearDownTestCaseWithSelector:unfocusedTestCaseSelector]; | ||
| } | ||
| @catch (NSException *exception) { | ||
| BOOL caseHadFailed = caseFailed; | ||
| caseFailed = YES; | ||
| // don't override `failureWasExpected` if we had already failed | ||
| BOOL exceptionWasExpected = [[self class] exceptionWasExpected:exception]; | ||
| if (!caseHadFailed) failureWasExpected = exceptionWasExpected; | ||
| [[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception] | ||
| expected:exceptionWasExpected]; | ||
| [self reportFailureInPhase:SLTestFailurePhaseTestCaseTeardown | ||
| toState:testCaseState | ||
| exception:exception | ||
| testCaseSelector:unfocusedTestCaseSelector]; | ||
| } | ||
|
|
||
| if (caseFailed) { | ||
| [[SLLogger sharedLogger] logTest:test caseFail:testCaseName expected:failureWasExpected]; | ||
| if (testCaseState.failed) { | ||
| [[SLLogger sharedLogger] logTest:test caseFail:testCaseName expected:testCaseState.failureWasExpected]; | ||
| numberOfCasesFailed++; | ||
| if (!failureWasExpected) numberOfCasesFailedUnexpectedly++; | ||
| if (!testCaseState.failureWasExpected) { | ||
| numberOfCasesFailedUnexpectedly++; | ||
| } | ||
| } else { | ||
| [[SLLogger sharedLogger] logTest:test casePass:testCaseName]; | ||
| } | ||
|
|
||
| numberOfCasesExecuted++; | ||
| } | ||
| } | ||
|
|
@@ -420,16 +433,17 @@ - (BOOL)runAndReportNumExecuted:(NSUInteger *)numCasesExecuted | |
| [self tearDownTest]; | ||
| } | ||
| @catch (NSException *exception) { | ||
| [[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception] | ||
| expected:[[self class] exceptionWasExpected:exception]]; | ||
| testDidFailInSetUpOrTearDown = YES; | ||
| [self reportFailureInPhase:SLTestFailurePhaseTestTeardown | ||
| toState:testState | ||
| exception:exception | ||
| testCaseSelector:NULL]; | ||
| } | ||
|
|
||
| if (numCasesExecuted) *numCasesExecuted = numberOfCasesExecuted; | ||
| if (numCasesFailed) *numCasesFailed = numberOfCasesFailed; | ||
| if (numCasesFailedUnexpectedly) *numCasesFailedUnexpectedly = numberOfCasesFailedUnexpectedly; | ||
|
|
||
| return !testDidFailInSetUpOrTearDown; | ||
| return !testState.failed; | ||
| } | ||
|
|
||
| - (void)wait:(NSTimeInterval)interval { | ||
|
|
@@ -468,9 +482,9 @@ - (NSException *)exceptionByAddingFileInfo:(NSException *)exception { | |
| return exception; | ||
| } | ||
|
|
||
| + (BOOL)exceptionWasExpected:(NSException *)exception { | ||
| return [[exception name] isEqualToString:SLTestAssertionFailedException]; | ||
| } | ||
| // Abstract | ||
| - (void)testDidEncounterFailure:(SLTestFailure *)failure {} | ||
|
|
||
|
|
||
| @end | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // | ||
| // SLTestCaseFailure.h | ||
| // Subliminal | ||
| // | ||
| // Created by Jacob Relkin on 6/20/14. | ||
| // Copyright (c) 2014 Inkling. All rights reserved. | ||
| // | ||
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| typedef NS_ENUM(NSUInteger, SLTestFailurePhase) { | ||
| SLTestFailurePhaseTestSetup, | ||
| SLTestFailurePhaseTestCaseSetup, | ||
| SLTestFailurePhaseTestCaseExecution, | ||
| SLTestFailurePhaseTestCaseTeardown, | ||
| SLTestFailurePhaseTestTeardown | ||
| }; | ||
|
|
||
| /** | ||
| SLTestFailure objects hold failure information for Subliminal test and test cases. | ||
| */ | ||
|
|
||
| @interface SLTestFailure : NSObject | ||
|
|
||
| /** | ||
| @param exception The exception that was thrown to cause this failure. | ||
| @param phase The phase in the test lifecycle in which the failure happened. | ||
| @param testCaseSelector The failed test case's selector. (can be NULL) | ||
| @return A new SLTestFailure object. | ||
| */ | ||
| + (instancetype)failureWithException:(NSException *)exception phase:(SLTestFailurePhase)phase testCaseSelector:(SEL)testCaseSelector; | ||
|
|
||
| /** | ||
| The phase in the test lifecycle in which the failure happened. | ||
| */ | ||
| @property (nonatomic, readonly, assign) SLTestFailurePhase phase; | ||
|
|
||
| /** | ||
| The failed test case's selector. (can be NULL) | ||
| */ | ||
| @property (nonatomic, readonly, assign) SEL testCaseSelector; | ||
|
|
||
| /** | ||
| The exception that was thrown to cause this failure. | ||
| */ | ||
| @property (nonatomic, readonly, strong) NSException *exception; | ||
|
|
||
| /** | ||
| If the failure was expected. | ||
| */ | ||
| @property (nonatomic, readonly, getter = isExpected) BOOL expected; | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // | ||
| // SLTestCaseFailure.m | ||
| // Subliminal | ||
| // | ||
| // Created by Jacob Relkin on 6/20/14. | ||
| // Copyright (c) 2014 Inkling. All rights reserved. | ||
| // | ||
|
|
||
| #import "SLTestFailure.h" | ||
| #import "SLTest.h" | ||
|
|
||
| @interface SLTestFailure () | ||
|
|
||
| @property (nonatomic, readwrite, strong) NSException *exception; | ||
| @property (nonatomic, readwrite, assign) SEL testCaseSelector; | ||
| @property (nonatomic, readwrite, assign) SLTestFailurePhase phase; | ||
|
|
||
| @end | ||
|
|
||
| @implementation SLTestFailure | ||
|
|
||
| + (instancetype)failureWithException:(NSException *)exception phase:(SLTestFailurePhase)phase testCaseSelector:(SEL)testCaseSelector { | ||
| SLTestFailure *failure = [self new]; | ||
| failure.phase = phase; | ||
| failure.exception = exception; | ||
| failure.testCaseSelector = testCaseSelector; | ||
| return failure; | ||
| } | ||
|
|
||
| - (BOOL)isEqual:(id)object { | ||
| if (![object isKindOfClass:[SLTestFailure class]]) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call. |
||
| return NO; | ||
| } | ||
|
|
||
| SLTestFailure *otherObject = object; | ||
| return (otherObject.phase == self.phase && | ||
| otherObject.testCaseSelector == self.testCaseSelector && | ||
| otherObject.isExpected == self.isExpected && | ||
| [otherObject.exception.name isEqualToString:self.exception.name] && | ||
| [otherObject.exception.reason isEqualToString:self.exception.reason]); | ||
| } | ||
|
|
||
| - (BOOL)isExpected { | ||
| return [self.exception.name isEqualToString:SLTestAssertionFailedException]; | ||
| } | ||
|
|
||
| @end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I admire the impulse to keep
SLTestas stateless as possible though it does make naming this method tricky, haha.What do you think about
-reportFailureWithException:inPhase:withTestCaseSelector:state:? The idea being to keep the test state parameters contiguous. This mirrorsSLTestFailure's initializer too.