Article summary
On my current project we’re using the Expecta matcher framework in our tests, allowing us to write clean, legible test expectations:
expect(foo).to.equal(bar);
expect(dwarves).to.haveCountOf(13);
expect(result).willNot.beNil();
What We Have
We frequently find ourselves checking that the contents of one array match those of another, but often don’t care about the order:
NSArray *expected = @[@"foo", @"bar", @"baz"];
NSArray *actual = @[@"bar", @"baz", @"foo"];
// this will mismatch:
expect(expected).to.equal(actual);
Here’s one approach to get the desired behavior:
NSCountedSet *expectedCountedSet = [NSCountedSet setWithArray:expected];
NSCountedSet *actualCountedSet = [NSCountedSet setWithArray:actual];
expect(expectedCountedSet).to.equal(actualCountedSet);
This does what we need, but it’s indirect and our intent isn’t clear at a glance.
##What We Want
What we really want is something like this:
expect(expected).to.equalInAnyOrder(actual);
It turns out that Expecta is extensible to allow us to do exactly that! Here’s what I came up with:
// based on EXPMatchers+beKindOf example from https://github.com/specta/expecta
#import "EXPMatchers+equalInAnyOrder.h"
EXPMatcherImplementationBegin(equalInAnyOrder, (id expected)) {
BOOL actualIsNil = (actual == nil);
BOOL expectedIsNil = (expected == nil);
//TODO: handle NSSet, NSCountedSet, ...
BOOL actualIsNSArray = [actual isKindOfClass:[NSArray class]];
BOOL expectedIsNSArray = [expected isKindOfClass:[NSArray class]];
prerequisite(^BOOL {
// Return `NO` if matcher should fail whether or not the result is inverted
// using `.Not`.
if(actualIsNil || expectedIsNil){
return NO;
}
if(!(actualIsNSArray && expectedIsNSArray)){
return NO;
}
return YES;
});
match(^BOOL {
NSCountedSet *expectedCountedSet = [NSCountedSet setWithArray:expected];
NSCountedSet *actualCountedSet = [NSCountedSet setWithArray:actual];
// Return `YES` if the matcher should pass, `NO` if it should not.
return [expectedCountedSet isEqual:actualCountedSet];
});
failureMessageForTo(^NSString * {
if (actualIsNil) {
return @"the actual value is nil/null";
}
if (expectedIsNil) {
return @"the expected value is nil/null";
}
if (!actualIsNSArray) {
return @"the actual value is not an NSArray";
}
if (!expectedIsNSArray) {
return @"the expected value is not an NSArray";
}
// Return the message to be displayed when the match function returns `YES`.
return [NSString stringWithFormat:@"expected: %@, got: %@", EXPDescribeObject(expected), EXPDescribeObject(actual)];
});
failureMessageForNotTo(^NSString * {
if (actualIsNil) {
return @"the actual value is nil/null";
}
if (expectedIsNil) {
return @"the expected value is nil/null";
}
if (!actualIsNSArray) {
return @"the actual value is not an NSArray";
}
if (!expectedIsNSArray) {
return @"the expected value is not an NSArray";
}
// Return the message to be displayed when the match function returns `NO`.
return [NSString stringWithFormat:@"expected: %@, got: %@", EXPDescribeObject(expected), EXPDescribeObject(actual)];
});
}
EXPMatcherImplementationEnd
## Conclusion
[Tests are documentation][tests_are_documentation], so we want them to be easy to read. Tests drive our development of production code, so we want them to be easy to write. Every now and then it’s worth a little effort to keep them that way.
[wiki_array]: http://en.wikipedia.org/wiki/Array_data_structure
[wiki_set]: http://en.wikipedia.org/wiki/Set_(abstract_data_type)
[wiki_multiset]: http://en.wikipedia.org/wiki/Set_(abstract_data_type)#Multiset
[apple_nscountedset]: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSCountedSet_Class/index.html
: https://gist.github.com/jrr/e75902242cc1422662a3
[tests_are_documentation]: http://ernststhoughts.blogspot.com/2010/03/automated-unit-tests-as-documentation.html
Nice. Can you write a custom matcher that takes another matcher? Something like the everyItem from Hamcrest, e.g.
expect(x).everyItem.to.beIn(y)
, reads pretty well and is very general.