Tag Cloud

Intro to OCHamcrest

Sometimes I feel like I write more test code than real code. For unit tests on iOS our stack is OCHamcrest, OCMock, and GHUnit. For functional tests, there’s nothing better than FoneMonkey. For this post, I’m going to focus on OCHamcrest.

Hamcrest was born in the Java world as the matcher framework in jMock. It was quickly extracted into its own framework and has become somewhat of a monster in the testing world. It’s now included directly in JUnit (since v4.4), and has been ported to many languages (OCHamcrest in Objective-C, Hamcrest-AS3 in Actionscript, PyHamcrest in Python, etc.). Additionally, the matcher concept is generally useful, and Hamcrest is used is lots of different places (my favorite is collection filtering with Hamcrest in LambdaJ).

When writing unit tests, OCHamcrest offers lots of advantages over the vanilla SenTest assertions. First, there’s a ton of matchers that really make life easy, especially when testing collections like NSArray. Second, OCHamcrest matchers are very readable in code, almost self-documenting. Lastly, OCHamcrest automatically provides excellent failure messages when actual is not equal to expected.

The last matcher may look a little weird, but remember matchers expect a matcher as their input param, and only default to equalTo if none is given. Thus, the first matcher hasItem(@"a") can be rewritten as hasItem(equalTo(@"a")).

We repeat the above example, but this time using numbers in our NSArray. As you can see below, all the number matchers require us to explicitly use equalToInt everywhere:

Matcher Error Messages

When a matcher fails, you get a standardize error message of: Expected "foo", but was "bar". This default message is easy to modify by using the describedAs() matcher in place of the typical is() matcher.

NSString*s =@"bar";
assertThat(s, is(@"foo"));
//Expected "foo", but was "bar"
assertThat(s, describedAs(@"doh! this should be 'foo'", equalTo(@"foo"), nil));
//Expected doh! this should be 'foo', but was "bar"
assertThat(s, describedAs(@"doh! this should be foo, %0, %1", equalTo(@"foo"), @"baz", [NSNumber numberWithInt:42], nil));
//Expected doh! this should be foo, "baz", <42>, but was "bar"

NOTE: The argument list for describedAs()MUST end with nil or your tests will crash instantly with no useful error message.

Building a Custom Matcher

Writing your own custom matchers is relatively easy. Here’s an example of a matcher that matches the value of some property on an object:

When we write a custom matcher, we must implement two methods, matches: to do the matching and describeTo: to provide feedback in case of match failure. In the above code, we first construct a selector from the given property name, then call the selector to get the actual property value, and finally check if actual matches the expected value (given by the valueMatcher).

@Cris: the short answer is no, I don’t believe that OCHamcrest has support for exception testing — just use STAssertThrows or STAssertNoThrow.

But honestly, I would never put exception testing in Objective-C into CI. If it’s not guaranteed to be repeatable, it has no place in CI. Period.

Cris Bennett

12.19.2011

8

I’m not entirely up to speed on Objective-C exceptions, since as even Apple seem to discourage their use by and large, I’ve tended to avoid doing anything with them.

What exactly to you mean by their not being repeatable? And, specifically in the case of calling `-verify` on a mock: under what circumstances would the exception not be thrown when it ought to be (ie. when the mocks expectations were violated)?

(Just curious).

Emilio

12.17.2012

9

Do you guys think that using NSAssert(which is throwing an exception at the end) is not recommended in Obj C? For example I use a lot STAssertThrows to check that certain methods don’t accept nil as parameters… Is there any potencial problem with that?