Boost logo

Boost :

Subject: [boost] [test] Interest in 'hamcrest'-style checks?
From: Jared Grubb (jared.grubb_at_[hidden])
Date: 2013-09-10 13:47:04


I recently read "Modern C++ Programming with Test-Driven Development", and I really enjoyed this book and highly recommend it. One of the styles I saw for the first time was "hamcrest"-style assertions. ("Hamcrest" is an anagram of "matchers"; also see http://en.wikipedia.org/wiki/Hamcrest.)

For a quick idea, imagine there is a BOOST_CHECK_THAT macro that looks like this: (excuse my choice of names here, we can pick better ones if people like the idea)

BOOST_CHECK_THAT( str, IsEqualTo(expStr) );
BOOST_CHECK_THAT( str, HasSubstring(needleStr) );
BOOST_CHECK_THAT( str, Not(HasSubstring(badStr)) );
BOOST_CHECK_THAT( vec, Contains(42) );

Comparing them to current boost::test assertions, usually the two are directly equivalent, and it's just a matter of style. At first, I was unconvinced of a need for this. However, there are a few scenarios where the hamcrest-style actually are really helpful:

BOOST_CHECK_THAT(vec, Any(Contains(42), Contains(-42)) );
BOOST_CHECK_THAT(str, All(StartsWith("abc"), HasSubstring("lmnop"), EndsWith("xyz")) );
BOOST_CHECK_THAT(count, All(GE(0), LT(10)) );
BOOST_CHECK_THAT(vec, HasSubsequence(otherVec) );
BOOST_CHECK_THAT(vec, Sorted(IsEqualTo({1, 2, 3})) );
BOOST_CHECK_THAT(optionalStr, Any(IsEqualTo(boost::none), Deref(IsEqualTo("str")));

Sure, almost all of this is already doable:

BOOST_CHECK_EQUAL_COLLECTIONS( /* sorry, but I really dislike this one :) */ );
BOOST_CHECK( std::count(vec.begin(), vec.end(), 42) && std::count(vec.begin(), vec.end(), -42) );

And, yes, you can create a helper functors today:

BOOST_CHECK( Contains(vec, 42) && Contains(vec, -42) );

But hamcrest still gives these advantages over even that:
 * Dont repeat yourself:
    -- the value under test appears only once in the check expression; this is really helpful if you want to test an expression ("future.get()") that can only be called once
 * Better diagnostics:
    -- when something fails, you can get a detailed error (eg: "blah does not contain the element blah" or "str does not start with blah")
    -- when you have "Any" conditions, "errors" get squashed if some other condition passes, but all are shown if none pass
    -- composed tests can provide nested diagnostics; eg: for All(A, Any(B,C)), you'd get something like "'...A...' is true, but none of { '...B...' , '...C...' } are true", but spread over many lines -- and even with indentation if we want
 * Intentional Asymmetry: Is it BOOST_CHECK_EQ(expected, actual) or (actual, expected)? It really doesnt matter in the end, but a message like "expected 34, got 33" is actually kinda nice. Putting the value under test at the start creates an asymmetry that helps guide the user to put the values in the "right" order.
 * Composability
 * Extendibility: once the facility is there, its actually really easy to write more ("CanLockMySemaphoreType(timeout)")

I wasnt able to find any discussion on boost's list for this, so I thought I'd bring it up.

As for difficulty, implementing the examples above is actually already quite easy using boost::test predicates, and the extra macro would be fairly simple to implement. Almost all the work would be in creating the library of matchers, which is the fun part :)

What do you think?

Jared


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk