Created
August 3, 2010 13:20
-
-
Save lukeredpath/506353 to your computer and use it in GitHub Desktop.
Enables simple and elegant testing of asynchronous/threaded code with OCUnit, blocks and OCHamcrest
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AssertEventually.h | |
// LRResty | |
// | |
// Created by Luke Redpath on 03/08/2010. | |
// Copyright 2010 LJR Software Limited. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
#import "HCMatcher.h" | |
#define kDEFAULT_PROBE_TIMEOUT 1 | |
#define kDEFAULT_PROBE_DELAY 0.1 | |
@protocol LRProbe <NSObject> | |
- (BOOL)isSatisfied; | |
- (void)sample; | |
- (NSString *)describeToString:(NSString *)description; | |
@end | |
@interface LRProbePoller : NSObject | |
{ | |
NSTimeInterval timeoutInterval; | |
NSTimeInterval delayInterval; | |
} | |
- (id)initWithTimeout:(NSTimeInterval)theTimeout delay:(NSTimeInterval)theDelay; | |
- (BOOL)check:(id<LRProbe>)probe; | |
@end | |
@class SenTestCase; | |
void LR_assertEventuallyWithLocationAndTimeout(SenTestCase *testCase, const char* fileName, int lineNumber, id<LRProbe>probe, NSTimeInterval timeout); | |
void LR_assertEventuallyWithLocation(SenTestCase *testCase, const char* fileName, int lineNumber, id<LRProbe>probe); | |
#define assertEventuallyWithTimeout(probe, timeout) \ | |
LR_assertEventuallyWithLocationAndTimeout(self, __FILE__, __LINE__, probe, timeout) | |
#define assertEventually(probe) \ | |
LR_assertEventuallyWithLocation(self, __FILE__, __LINE__, probe) | |
typedef BOOL (^LRBlockProbeBlock)(); | |
@interface LRBlockProbe : NSObject <LRProbe> | |
{ | |
LRBlockProbeBlock block; | |
BOOL isSatisfied; | |
} | |
+ (id)probeWithBlock:(LRBlockProbeBlock)block; | |
- (id)initWithBlock:(LRBlockProbeBlock)aBlock; | |
@end | |
#define assertEventuallyWithBlockAndTimeout(block,timeout) \ | |
assertEventuallyWithTimeout([LRBlockProbe probeWithBlock:block], timeout) | |
#define assertEventuallyWithBlock(block) \ | |
assertEventually([LRBlockProbe probeWithBlock:block]) | |
@interface LRHamcrestProbe : NSObject <LRProbe> | |
{ | |
id *pointerToActualObject; | |
id<HCMatcher> matcher; | |
BOOL didMatch; | |
} | |
+ (id)probeWithObjectPointer:(id *)objectPtr matcher:(id<HCMatcher>)matcher; | |
- (id)initWithObjectPointer:(id *)objectPtr matcher:(id<HCMatcher>)aMatcher; | |
- (id)actualObject; | |
@end | |
#define assertEventuallyThatWithTimeout(objectPtr, aMatcher, timeout) \ | |
assertEventuallyWithTimeout([LRHamcrestProbe probeWithObjectPointer:objectPtr matcher:aMatcher], timeout) | |
#define assertEventuallyThat(objectPtr, aMatcher) \ | |
assertEventually([LRHamcrestProbe probeWithObjectPointer:objectPtr matcher:aMatcher]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AssertEventually.m | |
// LRResty | |
// | |
// Created by Luke Redpath on 03/08/2010. | |
// Copyright 2010 LJR Software Limited. All rights reserved. | |
// | |
#import "AssertEventually.h" | |
#import <SenTestingKit/SenTestCase.h> | |
#import "HCStringDescription.h" | |
@interface LRTimeout : NSObject | |
{ | |
NSDate *timeoutDate; | |
} | |
- (id)initWithTimeout:(NSTimeInterval)timeout; | |
- (BOOL)hasTimedOut; | |
@end | |
@implementation LRTimeout | |
- (id)initWithTimeout:(NSTimeInterval)timeout | |
{ | |
if (self = [super init]) { | |
timeoutDate = [[NSDate alloc] initWithTimeIntervalSinceNow:timeout]; | |
} | |
return self; | |
} | |
- (void)dealloc | |
{ | |
[timeoutDate release]; | |
[super dealloc]; | |
} | |
- (BOOL)hasTimedOut | |
{ | |
return [timeoutDate timeIntervalSinceDate:[NSDate date]] < 0; | |
} | |
@end | |
#pragma mark - | |
#pragma mark Core | |
@implementation LRProbePoller | |
- (id)initWithTimeout:(NSTimeInterval)theTimeout delay:(NSTimeInterval)theDelay; | |
{ | |
if (self = [super init]) { | |
timeoutInterval = theTimeout; | |
delayInterval = theDelay; | |
} | |
return self; | |
} | |
- (BOOL)check:(id<LRProbe>)probe; | |
{ | |
LRTimeout *timeout = [[LRTimeout alloc] initWithTimeout:timeoutInterval]; | |
while (![probe isSatisfied]) { | |
if ([timeout hasTimedOut]) { | |
[timeout release]; | |
return NO; | |
} | |
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:delayInterval]]; | |
[probe sample]; | |
} | |
[timeout release]; | |
return YES; | |
} | |
@end | |
void LR_assertEventuallyWithLocationAndTimeout(SenTestCase *testCase, const char* fileName, int lineNumber, id<LRProbe>probe, NSTimeInterval timeout) | |
{ | |
LRProbePoller *poller = [[LRProbePoller alloc] initWithTimeout:timeout delay:kDEFAULT_PROBE_DELAY]; | |
if (![poller check:probe]) { | |
NSString *failureMessage = [probe describeToString:[NSString stringWithFormat:@"Probe failed after %d second(s). ", (int)timeout]]; | |
[testCase failWithException: | |
[NSException failureInFile:[NSString stringWithUTF8String:fileName] | |
atLine:lineNumber | |
withDescription:failureMessage]]; | |
} | |
[poller release]; | |
} | |
void LR_assertEventuallyWithLocation(SenTestCase *testCase, const char* fileName, int lineNumber, id<LRProbe>probe) | |
{ | |
LR_assertEventuallyWithLocationAndTimeout(testCase, fileName, lineNumber, probe, kDEFAULT_PROBE_TIMEOUT); | |
} | |
#pragma mark - | |
#pragma mark Block support | |
@implementation LRBlockProbe | |
+ (id)probeWithBlock:(LRBlockProbeBlock)block; | |
{ | |
return [[[self alloc] initWithBlock:block] autorelease]; | |
} | |
- (id)initWithBlock:(LRBlockProbeBlock)aBlock; | |
{ | |
if (self = [super init]) { | |
block = Block_copy(aBlock); | |
isSatisfied = NO; | |
[self sample]; | |
} | |
return self; | |
} | |
- (void)dealloc | |
{ | |
Block_release(block); | |
[super dealloc]; | |
} | |
- (BOOL)isSatisfied; | |
{ | |
return isSatisfied; | |
} | |
- (void)sample; | |
{ | |
isSatisfied = block(); | |
} | |
- (NSString *)describeToString:(NSString *)description; | |
{ | |
// FIXME: this is a bit shit and non-descriptive | |
return [description stringByAppendingString:@"Block call did not return positive value."]; | |
} | |
@end | |
#pragma mark - | |
#pragma mark Hamcrest support | |
@implementation LRHamcrestProbe | |
+ (id)probeWithObjectPointer:(id *)objectPtr matcher:(id<HCMatcher>)matcher; | |
{ | |
return [[[self alloc] initWithObjectPointer:objectPtr matcher:matcher] autorelease]; | |
} | |
- (id)initWithObjectPointer:(id *)objectPtr matcher:(id<HCMatcher>)aMatcher; | |
{ | |
if (self = [super init]) { | |
pointerToActualObject = objectPtr; | |
matcher = [aMatcher retain]; | |
[self sample]; | |
} | |
return self; | |
} | |
- (void)dealloc | |
{ | |
[matcher release]; | |
[super dealloc]; | |
} | |
- (BOOL)isSatisfied; | |
{ | |
return didMatch; | |
} | |
- (void)sample; | |
{ | |
didMatch = [matcher matches:[self actualObject]]; | |
} | |
- (NSString *)describeToString:(NSString *)description; | |
{ | |
HCStringDescription* stringDescription = [HCStringDescription stringDescription]; | |
[[[[stringDescription appendText:@"Expected "] | |
appendDescriptionOf:matcher] | |
appendText:@", got "] | |
appendValue:[self actualObject]]; | |
return [description stringByAppendingString:[stringDescription description]]; | |
} | |
- (id)actualObject | |
{ | |
return *pointerToActualObject; | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@implementation ExampleTests // inherits from SenTestCase | |
// all examples default to 5 second timeout | |
- (void)testSomeAsynchronousCodeWithHamcrestMatcher | |
{ | |
[asyncCommand doCommandThenCallSelectorInTheFuture:@selector(inTheFuture:)]; | |
// we give the assertion an object pointer so it can be dereferenced at | |
// runtime to see if is pointing to a new object (assume its usually nil to start with) | |
assertEventuallyThat(&receivedString, is(equalTo(@"expected result"))); | |
} | |
- (void)testSomeAsynchronousCodeWithBlockMatcher | |
{ | |
[asyncCommand doCommandThenCallSelectorInTheFuture:@selector(inTheFuture:)]; | |
assertEventuallyWithBlock(^{ | |
return [self.receivedString equalToString:@"expected result"]; | |
}); | |
} | |
#pragma mark support | |
- (void)inTheFuture:(NSString *)result // called by asyncCommand | |
{ | |
self.receivedString = result; | |
} | |
@end |
Fixed a serious issue with the polling code; using [NSThread sleepForTimeInterval] is not a good idea as it blocks the test case thread (the main thread) and any asynchronous code that relies on the main thread runloop (like NSURLConnection). Spinning the runloop is better.
[timeoutDate timeIntervalSinceDate:[NSDate date]]
should be written as [timeoutDate timeIntervalSinceNow]
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Above is an implementation of the Probe/Poller idea as described and implemented in Growing Object-Oriented Software, Guided by Tests by Nat Pryce and Steve Freeman.
It allows the use of "probe" objects (in this implementation, any object that implements the LRProbe protocol) to check certain conditions are met within a timeout. The probes are polled regularly (default delay is 0.1 seconds) with the idea being that your test should pass as fast as possible and timeout if the probe is never satisfied.
You can roll your own probes easily enough, but the above implementation comes with baked-in support for Objective-C block-based probes or Hamcrest matcher probes. See the example for usage.