Skip to content

Instantly share code, notes, and snippets.

@lukeredpath
Created August 3, 2010 13:20
Show Gist options
  • Save lukeredpath/506353 to your computer and use it in GitHub Desktop.
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
//
// 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])
//
// 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
@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
@lukeredpath
Copy link
Author

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.

@newacct
Copy link

newacct commented Oct 27, 2011

[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