Skip to content

Writing Custom Matchers

Jon Reid edited this page Oct 10, 2021 · 12 revisions

The following is a quick introduction. For a more comprehensive guide, see How to Make Your Own OCHamcrest Matcher for Powerful Asserts.

OCHamcrest comes bundled with lots of useful matchers, but you'll probably find that you need to create your own from time to time to fit your testing needs. This commonly occurs when you find a fragment of code that tests the same set of properties over and over again (and in different tests), and you want to bundle the fragment into a single assertion. By writing your own matcher you'll eliminate code duplication and make your tests more readable!

Let's write our own matcher for testing if a calendar date falls on a Saturday. This is the test we want to write:

- (void)testDateIsOnASaturday
{
    NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
    dateComponents.day = 26;
    dateComponents.month = 4;
    dateComponents.year = 2008;
    NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    NSDate *date = [gregorianCalendar dateFromComponents:dateComponents];

    assertThat(date, is(onASaturday()));
}

Here's the interface:

#import <OCHamcrest/HCBaseMatcher.h>

// Matches dates that fall on a given day of the week.
@interface IsGivenDayOfWeek : HCBaseMatcher

@property (nonatomic, readonly, assign) NSInteger dayOfWeek; // Sunday is 1, Saturday is 7

- (instancetype)initWithDayOfWeek:(NSInteger)dayOfWeek;

@end


// Factory function to generate Saturday matcher.
FOUNDATION_EXPORT id onASaturday(void);

The interface consists of two parts: a class definition, and a factory function (with C binding). Here's what the implementation looks like:

#import "IsGivenDayOfWeek.h"

static NSString* const dayAsString[] =
        { @"ZERO", @"Sunday", @"Monday", @"Tuesday", @"Wednesday", @"Thursday", @"Friday", @"Saturday" };

@implementation IsGivenDayOfWeek

- (instancetype)initWithDayOfWeek:(NSInteger)dayOfWeek
{
    self = [super init];
    if (self)
        _dayOfWeek = dayOfWeek;
    return self;
}

// Test whether item matches.
- (BOOL)matches:(id)item
{
    if (![item isKindOfClass:[NSDate class]])
        return NO;

    NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    return [gregorianCalendar component:NSCalendarUnitWeekday fromDate:item] == self.dayOfWeek;
}

// Describe the matcher.
- (void)describeTo:(id <HCDescription>)description
{
    [[description appendText:@"date falling on "] appendText:dayAsString[self.dayOfWeek]];
}

@end


id onASaturday(void)
{
    return [[IsGivenDayOfWeek alloc] initWithDayOfWeek:7];
}

For our Matcher implementation we implement the -matches: method (which determines the weekday after confirming the argument is a NSDate) and the -describe_to: method (which is used to produce a failure message when a test fails). Here's an example of failure:

    NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
    dateComponents.day = 6;
    dateComponents.month = 4;
    dateComponents.year = 2008;
    NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
    NSDate *date = [gregorianCalendar dateFromComponents:dateComponents];

    assertThat(date, is(onASaturday()));

fails with the message

Expected calendar date falling on Saturday, but was <2008-04-06 07:00:00 +0000>

and Xcode shows it as a test error. Clicking the error message takes you to the assertion that failed.

Even though the onASaturday function creates a new matcher each time it is called, you should not assume this is the only usage pattern for your matcher. Therefore you should make sure your matcher is stateless, so a single instance can be reused between matches.

Clone this wiki locally