How does it work - Expecta
16 Mar 2014##tl;dr
Follow me as I poke Expecta with a stick to see how it works - it’s pretty cool.
It’s been a long time since my last post but I thought I’d get stuck into how something that appears simple actually works. That subject will be Expecta matchers. If you don’t know what Expecta is then you might want to nip over to the Github repo and skim the Readme then we can get stuck in.
Take the following example:
This line looks so simple but it’s hiding a lot of clever techniques that may or may not be useful to keep within your own development bag of tricks.
Let’s break this apart and demystify what’s really going on.
##expect
expect() looks like a function call, so you may assume that there is a function declared somewhere called expect. Thankfully that is not the case or this blog post would be very boring - instead we find this defined as:
# define expect(actual) EXP_expect((actual))
this in turn is declared as
#define EXP_expect(actual) _EXP_expect(self, __LINE__, __FILE__, ^id{ return EXPObjectify((actual)); })
_EXP_expect is the actual function we was looking for.
At this point you may be wondering why did they bother with this odd chain of macro expansions. The logical reason would most likely be that expect(id actual) is an optional short hand syntax, which is only enabled by defining EXP_SHORTHAND before importing Expecta.h. Without this define you have to use the long hand EXP_expect(id actual) and this is what expands to _EXP_expect with all the additional arguments.
Go ahead and reread that last paragraph a few times if it didn’t sink in the first time.
In effect the define for expect saves you from having to type out
every time you want to set up an expectation.
The _EXP_expect function simply creates a new instance of EXPExpect with all the arguments supplied. I’m not going to go over the EXPExpect class as I want to cover the single line of code at the top of this post.
Before we move on though it’s worth pointing out that Expecta is really cool as it does not require you to box your arguments. It’s the EXPObjectify function that does the work of making sure that if you pass in a primitive like int
, float
, double
, etc then it will box it with an NSValue
or NSNumber
automatically for you.
##to
to looks simple enough - so why is this interesting? Well knowing that it is used like this to.equal...
leads us to the conclusion that it returns an instance of something that responds to equal
. Before following the link to look up the definition keep in mind that to is entirely optional and I could validly call expect(2).equal(2);
- this should narrow down what to returns.
Yup you may have guessed it to returns an instance of EXPExpect - not just any instance but the instance it was called on - check it out:
This is just a little sprinkling of syntactic sugar. It makes the expectation read better consider expect(2).to.equal(2)
vs expect(2).equal(2)
.
##equal
By now you are probably thinking that the interesting stuff is over and equal
will just be a property declared on EXPExpect
. You may also come to the conclusion that the property will return a pointer to either a block or a function so that it can be invoked with parentheses and an argument equal(2)
. This is exactly what is happening - kind of…
If you search the EXPExpect class you will not find a property declaration but if you follow the declarationf of equal
through you’ll land in EXPExpect+equal.h, which looks like this:
This is where we have to make sure our brain is really engaged and step up a gear. Take a breather and join me after the relaxing grey line…
EXPMatcherInterface ends up mapping through to
which in the case of our equal
will expand to
In english this has declared a named category called _equalMatcher
on the EXPExpect class. This category declares a single readonly property, which means that in the .m
file we would expect to see a single method declared with the signature - (void(^)(id expected))_equal;
NB I showed the mapping of _equal as equal is only used for code completion and there is in fact never an implementation declared for - (void(^)(id expected))equal;
So being inquisitive we jump to the implementation to see how this method is defined and we find more #define
magic.
When the two macros are expanded we end up with this (formatting mine):
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
__attribute__((constructor)) static void EXPFixCategoriesBugEXPMatcher_equalMatcher() {};
@implementation EXPExpect (_equalMatcher)
@dynamic _equal;
- (void (^)(id expected))_equal {
EXPBlockDefinedMatcher *matcher = [[EXPBlockDefinedMatcher alloc] init];
[[[NSThread currentThread] threadDictionary] setObject:matcher forKey:@"EXP_currentMatcher"];
__block id actual = self.actual;
__block void (^prerequisite)(EXPBoolBlock block) = ^(EXPBoolBlock block) { EXP_prerequisite(block); };
__block void (^match)(EXPBoolBlock block) = ^(EXPBoolBlock block) { EXP_match(block); };
__block void (^failureMessageForTo)(EXPStringBlock block) = ^(EXPStringBlock block) { EXP_failureMessageForTo(block); };
__block void (^failureMessageForNotTo)(EXPStringBlock block) = ^(EXPStringBlock block) { EXP_failureMessageForNotTo(block); };
prerequisite(nil); match(nil); failureMessageForTo(nil); failureMessageForNotTo(nil);
void (^_equal) (id expected) = [^ (id expected) {
{
match(^BOOL{
if((actual == expected) || [actual isEqual:expected]) {
return YES;
} else if([actual isKindOfClass:[NSNumber class]] && [expected isKindOfClass:[NSNumber class]]) {
if(EXPIsNumberFloat((NSNumber *)actual) || EXPIsNumberFloat((NSNumber *)expected)) {
return [(NSNumber *)actual floatValue] == [(NSNumber *)expected floatValue];
}
}
return NO;
});
failureMessageForTo(^NSString *{
return [NSString stringWithFormat:@"expected: %@, got: %@", EXPDescribeObject(expected), EXPDescribeObject(actual)];
});
failureMessageForNotTo(^NSString *{
return [NSString stringWithFormat:@"expected: not %@, got: %@", EXPDescribeObject(expected), EXPDescribeObject(actual)];
});
}
[self applyMatcher:matcher to:&actual];
} copy];
_EXP_release(matcher);
return _EXP_autorelease(matcherBlock);
}
@end
Yup that’s right there are line numbers in this listing as it’s a big one.
So let’s distill what this category method is actually doing:
- Creating an instance of EXPBlockDefinedMatcher (line 8)
- Setting up blocks to enable this instance to be configured with a DSL like syntax - prerequisite (line 11), match (line 12), failureMessageForTo (line 13) and failureMessageForNotTo (line 14)
- Ensure that these are all initialised to
nil
(line 15) - Configuring this instance’s properties with blocks for match (line 18), failureMessageForTo (line 29) and failureMessageForNotTo (line 33).
- Ensures that [self applyMatcher:matcher to:&actual]; (line 37) is executed at the end of the block that is declared (line 16) and returned (line 40).
If we didn’t want to use the macros (not advised at all - only shown for interest sake) then the implementation could remove the added complexity of setting up the DSL and end up with an implementation like this:
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
- (void (^)(id expected))_equal {
__block id actual = self.actual;
return [[^(id expected) {
EXPBlockDefinedMatcher *matcher = [[EXPBlockDefinedMatcher alloc] init];
matcher.matchBlock = ^BOOL{
if((actual == expected) || [actual isEqual:expected]) {
return YES;
} else if([actual isKindOfClass:[NSNumber class]] && [expected isKindOfClass:[NSNumber class]]) {
if(EXPIsNumberFloat((NSNumber *)actual) || EXPIsNumberFloat((NSNumber *)expected)) {
return [(NSNumber *)actual floatValue] == [(NSNumber *)expected floatValue];
}
}
return NO;
};
matcher.failureMessageForToBlock = ^NSString *{
return [NSString stringWithFormat:@"expected: %@, got: %@", EXPDescribeObject(expected), EXPDescribeObject(actual)];
};
matcher.failureMessageForNotToBlock = ^NSString *{
return [NSString stringWithFormat:@"expected: not %@, got: %@", EXPDescribeObject(expected), EXPDescribeObject(actual)];
};
[self applyMatcher:matcher to:&actual];
} copy] autorelease];
}
This version seems like an awful lot of error prone boiler plate code that a developer would have to write for each matcher. Keep in mind that there are ~25 matchers included with Expecta and you can define your own.
Let’s list the required steps for this implementation:
- Creating an instance of EXPBlockDefinedMatcher (line 5)
- Configuring this instance’s properties with blocks for
matchBlock
(line 7),failureMessageForToBlock
(line 18), andfailureMessageForNotToBlock
(line 22). - Ensures that
[self applyMatcher:matcher to:&actual];
is executed at the end of the block that is declared and returned (line 3).
In this implementation there are only 3 steps so this must surely be better?
Nope:
- In the original list of 5 steps only step 4 was exposed to the developer and the remaining steps were hidden behind macros.
- In this implementation the developer has to know about all 3 steps.
- This means that the developer has 2 extra steps to remember to do and to make matters worse they are wrapping (before + after) steps.
- In the first listing the developer is literally just stating the test requirements, whereas in this version the developer has to know about matchers and how they need to be configured in addition to the test requirements.
With all this knowledge we can now see that when we invoked equal(2)
this gets expanded with #define equal(…) _equal(EXPObjectify((__VA_ARGS__))) to _equal(2)
, which is the name of the method that was added with a category on EXPExpect.
So hopefully now I’ve pulled back the curtain a little the line
won’t seem as mysterious.
##Wrapping up
Well that was a heavy post with a lot to understand and I do apologise if I got any of it wrong - I’m no Expecta expert. Reading code is great fun especially when you get that Eureka moment and you learn something new. The joy of a project like Expecta is that it is all unit tested so you can hack around and change things to test your assumptions - hit test and wait for your theory to be validated with a sea of red or green unit test results.