Expressive Tests with Hamcrest

Hamcrest is a set of matchers for writing more expressive code. It just so happens that these matchers are especially useful when writing tests. In this article, we'll look at Hamcrest for PHP.


What is Hamcrest?

Every Hamcrest matcher helps you write tests that read very naturally.

Hamcret's expressiveness originated with JMock, but it wasn't until the addition of the unique assertThat() method that it was refactored into a self-contained library and independently usable in testing frameworks.

After its initial Java adoption, Hamcrest implementations in several programming languages became available. JUnit, RSpec and other testing frameworks implement native Hamcrest syntax, removing the need to explicitly include any libraries. Due to Hamcret's fast adoption, testing frameworks were re-categorized to the following:

  • First generation testing frameworks were very basic, having a single assert() method with usage like: assert(x==y). Programmers had difficulties writing expressive and well organized tests. It also required programming knowledge to understand more complex conditions and made writing more difficult.
  • Second generation testing frameworks, like PHPUnit, offer a large set of different assertions. These frameworks extracted the action or predicate from the parameters (x == y) into the names of the assertions, such as: assertEquals($expected, $actual). This made tests more expressive and made it easy to define custom assertion functions.
  • Third generation testing frameworks use a single assertion method (assertThat()) in conjunction with expressive matchers, making assertions read like English sentences: assertThat($calculatedResult, equalTo($expectedResult)) in contrast with assertEquals($expectedResult, $calculatedResult).

Using Hamcrest matchers can also help in other ways; you can write your custom matchers and use them inside the assertThat() function. Hamcret also provides much more information when something goes wrong. Instead of an obscure message like "Expected value is not True", Hamcrest errors actually tells all values involved with the test--that is, both expected and actual values. The matchers also allow flexible assertions, so tests do not fail after making small modifications that should not break the test. In other words, fragile tests are more robust.


Installing Hamcrest for PHP

There are several ways to install Hamcrest. The two most common involve using PEAR or downloading the source code. At the time of this writing, Hamcrest for PHP is not yet available through Composer.

Use PEAR

Using PEAR to install Hamcrest is easy. Simply run the following commands:

Make sure to check that you have PEAR's installation folder in you global path. This makes it easy to include Hamcrest in your tests.

Download Source Archive

You can always download the latest version of Hamcrest from the project's download page and use it like any third party PHP library.


Our First Test

Let's first ensure we have a working skeleton test with Hamcrest enabled. Create a project in your favorite IDE or code editor and create a test file. I just created a new project in NetBeans with a folder called Test as the main folder. Inside this folder is an empty file called HamcrestMatchersForPHPTest.php. This will be the test file, and its contents are the following:

The first line includes the Hamcrest library. Please note that it is with capital "H" for both the folder and the file name. Hamcrest's official readme file contains a typo.

Next, our only test, testHamcrestWorks(), asserts that a is equal to a. Obviously, this is a passing test.

Passing Test

This first simple example is natural to read. Assert that 'a' is equal to 'a'. It almost needs no explanation.

The only assertion method we will use in our tests is assertThat(). The matcher is() is just syntactic sugar; it does nothing except construct your sentence. Finally the matcher equalTo() compares the first parameter to assertThat() with value supplied to equalTo(). This test effectively translates to 'a' == 'a'.


Comparing Numbers

Let's start with a simple example:

You can write your custom matchers and use them inside the assertThat() function.

This code introduces new matchers; the first two are lessThan() and greaterThan(). These matchers are the equivalent to the "<" and ">" comparison operators.

The identicalTo() provides another means of checking equality. In fact, this is the equivalent of the === operator, whereas equalTo() is ==. Another new matcher in this code is comparesEqualTo(), effectively performing the equivalent of !($x $y). In the above code, both $x and $y are the value 2 (so the test passes).

One of the most interesting matchers is closeTo(). It accepts two arguments: the target value and the allowed difference, respectively. This example checks if 2 is close to 3 by a maximum of 1. This is obviously correct, and the assertion passes.

Finally, the last assertion is just a simple negation combined with is(). This obviously asserts that 2 is not 3.

Combined Number Matchers

Hamcrest also provides matchers that equate to the <= and >= comparison operators. They're aptly named lessThanOrEqualTo() and greaterThanOrEqualTo(), and they're used like this:

Hamcrest also provides the atMost() and atLeast() matchers. The lessThanOrEqualTo() and atMost() matchers are identical. They both equate to $x . Naturally, the greaterThanOrEqualTo() and atLeast() perform the exact opposite, checking for $x >= $y.


Working with Strings

Hamcrest also provides several matchers for working with strings. Here are some examples:

I recommend you use the more expressive matchers whenever possible...

Obviously, the equalTo() and identicalTo() matchers work with strings, and they behave exactly as you would expect them to. But as you can see, Hamcrest provides other string-specific equality matchers. As their names imply, the equalToIgnoringCase() and equalToIgnoringWhiteSpace() matchers match strings by ignoring case and whitespace, respectively.

Other matchers, such as startsWith() and endsWith(), check if the specified sub-string is at the beginning or end of the actual string. The containsString() matcher checks if the string contains the supplied sub-string. The containsString() matcher can also be extended with containsStringIgnoringCase(), adding case insensitivity.

The matchesPattern() matcher incorporates regular expressions to find a match in the string. You can use any regular expression, and in many cases, this solution is necessary in more complex strings. In any case, I recommend you use the more expressive matchers whenever possible and only resort to regular expressions if absolutely necessary; doing so makes your code more readable by everyone.

Matching Empty Strings

It's common to test if a string is empty. Hamcrest has you covered.

Yes, there are many matchers for checking if a string is empty. Each variant has a version with "is" in front of it. Essentially, emptyString() and isEmptyString() are identical, and the same is true for the other matchers. The nonEmptyString() and isNonEmptyString() matchers can also be written like this:

But of course, those variants can add extra verbosity, making it more difficult to understand the code at first glance.


Inclusions and Exclusions

Here are some nice approaches to determine whether or not a variable belongs to a group:

These examples use strings, but you can use these matchers with variables like objects, arrays, numbers, etc. The anyOf() and noneOf() matchers determines whether or not the expected variable resides in the provided list of values.

The other two matchers, both() and either(), are commonly used with the andAlso() and orElse() modifiers. These are equivalents of:

Finally, anything() matches... well, anything. It has an optional string parameter for meta data purposes, helping anyone reading the test to better understand why an assertion should always match.


Arrays

The array matchers are probably the most complex and useful matchers provided by Hamcrest. The code in this section provides a list of tricks to make complicated array-based assertions change into code that reads like well written prose.

Please Note: I make no difference between arrays and hashes in these examples. I only talk about arrays, but everything applies to hashes as well.

Array Equality

These methods highlight different ways to compare the equality of two arrays. The first version uses the misleadingly named anArray() matcher. It actually it compares the two arrays element by element. On failure, only the first set of unequal elements are displayed in the error message.

The second version, using equalTo(), also compares each element in the arrays, but it outputs both arrays in their entirety on failure. Naturally, the length of your arrays will determine which matcher you use. Reading large arrays can be difficult.

Partial Array Matches

In many cases, we simply want to check if an array contains certain elements. Hamcrest has us covered.

The hasItemInArray() and hasValue() matchers are identical; they both check if the provided value or matcher result exists in the array. Providing a value as an argument is equivalent to using equalTo(). Therefore, these are identical: hasValue(2) and hasValue(equalTo(2)).

The next two matchers, arrayContaining() and contains(), are also identical. They check, in order, that every element of the array satisfies the matcher, or that every element is equal to the specified values.

Finally, as you can easily deduce from the above example, arrayContainingInAnyOrder() and containsInAnyOrder() are the same as the previous two matchers. The only difference is that they do not care about the order.

Matching Array Keys

The matchers hasKeyInArray() and hasKey() check if the given argument matches any keys in the array, while the last two matchers return true only if both key and value are found. So, a matcher like hasEntry('one', 2); would have failed our test because in our array, at key 'one' we have the value 1 and not 2.

Please Note: It is recommended to use the shorter version (hasKey) when it is obvious from the variable's name that it is an array. Whoever reads your code may be confused about the type of the variable, in which case, (hasKeyInArray) may be more helpful.

Array Sizes

You can easily check the size of an array with three matchers:

The arrayWithSize() matcher checks for a specific size, nonEmtpyArray() checks if the array has at least one element, and emptyArray() verifies that the given array is empty.

Please Note: There are versions of these three matchers if you prefer iterable objects. Just replace "Array" with "Traversable" like this traversableWithSize().


Checking for Value Types

These are the last matchers. You can practically check for any data type in PHP.

These are self-explanatory. Simply provide a value or object, and use one of the easy-to-use matchers to determine its data type.


Conclusions

Hamcrest PHP is not extensively documented in the official documentation, and I hope this tutorial helped explain the matchers it provides. There are a few matchers not listed in this tutorial, but they are very exotic and rarely used.

Every Hamcrest matcher helps you write tests that read very naturally.

If they are not enough for you, you can add your own matchers for your whatever situation you need them for. I hope you enjoyed this tutorial, and thank you for reading.

Tags:

Comments

Related Articles