At Pivotal we write a lot of tests, or specs if you prefer. We TDD nearly everything we write, in every language we write in and on every platform we write for, so we actively work to improve every aspect of our testing tools. Personally, as I’ve written tests in Objective C I’ve found that the syntax of expectations has left much to be desired.
expect(composer.name).toEqual("Ludwig"); expect(composer.symphonies.count).toEqual(9); expect(composer.symphonies).toContain("Eroica"); expect(composer.symphonies).not.toContain("Appassionata");
In comparison, the same expectations in Objective C, using OCHamcrest:
assertThat(composer.name, equalTo(@"Ludwig")); assertThatUnsignedInt([composer.symphonies count], equalToUnsignedInt(9u)); assertThat(composer.symphonies, hasItem(@"Eroica")); assertThat(composer.symphonies, isNot(hasItem(@"Appassionata"));
I want three primary attributes from matchers: readability, ease of use, and extensibility.
With the exception of the unfortunate mismatch between the negation function (
isNot) and the containment matcher (
hasItem), I find the OCHamcrest expectations reasonably readable. The distinction between the first equality matcher (
assertThat/equalTo) and the second (
assertThatUnsignedInt/equalToUnsignedInt) is a bit jarring, though.
Ease of use
That distinction is much more of a problem for usability. The problem comes from the Objective C type system’s split personality: some variables refer to Objective C objects (NSNumber *, NSString *, NSArray *, UIView *), while the others refer to built-in types and structs (int, char *, CFArray, CGRect), and never the twain shall meet. A simple equality match must use the
isEqual: method for the former, but the
== operator, or something more esoteric like
strncmp, for the latter. Sadly, a single matcher function has no way to determine the correct equality mechanism to use. Worse, the matcher function declarations must specify the types of their parameters, so expectations for any non-pointer types must have separate declarations for each type. The number of functions for any general purpose expectation explodes:
I can’t describe how many times I’ve failed to have the type of the
assertThatX function for an expectation match the
equalToX function. Frustration ensues.
Adding new OCHamcrest matchers isn’t terribly difficult, but you have to derive your matcher object from the HCBaseMatcher class, which means any library that includes custom matchers must link against the OCHamcrest library. If your spec target links against OCHamcrest and your custom matcher library, and your custom matcher library also links against OCHamcrest, suddenly you’re in dependency management hell.
What to do about it?
We’ve used OCHamcrest on a number of projects now, but the usability issues have become an increasing burden.
Peter Kim wrote a nice matcher library named Expecta which solves the type differentiation problem using the Objective C
@encode function. We’ve used this on a couple projects, and like it more than OCHamcrest. The above example would look like this in Expecta:
expect(composer.name).toEqual(@"Ludwig"); expect([composer.symphonies count]).toEqual(9); expect(composer.symphonies).toContain(@"Eroica"); expect(composer.symphonies).Not.toContain(@"Appassionata");
At the same time, we added a set of built-in matchers to Cedar, which solve the type differentiation problem with C++ templates, and which we designed to be extremely extensible. We’re using these matchers on some of our projects as well, and we’ve found they work nicely. Here’s the above example using Cedar matchers:
expect(composer.name).to(equal(@"Ludwig")); expect([composer.symphonies count]).to(equal(9)); expect(composer.symphonies).to(contain(@"Eroica")); expect(composer.symphonies).to_not(contain(@"Appassionata"));
You can read more about the Cedar matches, including how to use and extend them, here.