Skip to content

Instantly share code, notes, and snippets.

@lukeredpath
Created August 3, 2010 13:20
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • 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

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.

@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