public
Last active

Enables simple and elegant testing of asynchronous/threaded code with OCUnit, blocks and OCHamcrest

  • Download Gist
AssertEventually.h
Objective-C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
//
// 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
Objective-C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
//
// 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
ExampleTestCase.m
Objective-C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
@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

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.

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]

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.