Slide 1

Slide 1 text

Write more expressive tests with Hamcrest Gareth Ellis NomadPHP Lightning Talk, October 2016

Slide 2

Slide 2 text

What is Hamcrest?

Slide 3

Slide 3 text

Matcher library originally written in Java, ported to PHP Allows for more expressive assertions in your code

Slide 4

Slide 4 text

PHPUnit: $this->assertEquals(“2”, $someValue); Hamcrest: assertThat($someValue, is(equalTo(“2”)));

Slide 5

Slide 5 text

$food = $data_source->getFood(); $fruit = [ 'orange', 'apple', 'pear', ]; $this->assertEquals(0, count(array_diff($food, $fruit))); $this->assertEquals(0, count(array_diff($fruit, $food))); assertThat($food, containsOnlyValuesFrom($fruit)); Real-world example

Slide 6

Slide 6 text

assertThat(3, is(greaterThan(2))); assertThat($someValue, is(atMost(99))); Number matchers!

Slide 7

Slide 7 text

assertThat($someString, is(equalToIgnoringCase(“A sTrIng”))); assertThat($someString, containsString($subString)); assertThat($someString, startsWith(“foo”)); String matchers!

Slide 8

Slide 8 text

assertThat($object, is(anInstanceOf(SomeClass::class))); assertThat(“string”, is(stringValue())); assertThat(123, is(not(stringValue()))); Type matchers!

Slide 9

Slide 9 text

assertThat($option, is(anyOf($arrayOfOptions))); assertThat($option, is(noneOf($arrayOfBlackListedOptions))); Inclusion/exclusion matchers!

Slide 10

Slide 10 text

assertThat($array, contains($someValue)); assertThat($array, is(arrayContainingInAnyOrder([“foo”, “bar”]))); assertThat($array, is(arrayWithSize(4))); Array matchers!

Slide 11

Slide 11 text

assertThat(“a string”, is(both(stringValue())->andAlso(not(emptyString()))); Chained matchers!

Slide 12

Slide 12 text

$ composer require hamcrest/hamcrest-php Usage

Slide 13

Slide 13 text

$ composer require –dev graze/hamcrest-test-listener With PHPUnit (optional) In phpunit.xml: Allows PHPUnit to listen for Hamcrest assertions

Slide 14

Slide 14 text

require “vendors/hamcrest/hamcrest-php/hamcrest/Hamcrest.php”; Include Hamcrest’s global functions Or autoload via composer.json: { “autoload”: { “files”: [ “./vendors/hamcrest/hamcrest-php/hamcrest/Hamcrest.php” ] } }

Slide 15

Slide 15 text

Maximum expressiveness Custom matchers

Slide 16

Slide 16 text

$redirectUrl = “/my_app/some_page?foo=bar”; $this->assertEquals(302, $controller->response->statusCode()); $headers = $controller->response->header(); $this->assertArrayHasKey($headers, “Location”); $this->assertEquals($headers[“Location”], $redirectUrl); Have you ever done anything like this in a test?

Slide 17

Slide 17 text

$redirectUrl = “/my_app/some_page?foo=bar”; assertThat($controller, redirectedToUrl($redirectUrl)); Wouldn’t it be much nicer to do this instead? assertThat() takes any value as its first argument… …and an instance of \Hamcrest \Matcher as its second argument Functions exist only as expressive factories for instantiating matchers

Slide 18

Slide 18 text

class Cake2ControllerRedirectMatcher extends \Hamcrest\BaseMatcher { private $url; public function __construct($url) { $this->url = $url; } public function matches($controller) { } public function describeTo(Description $description) { } }

Slide 19

Slide 19 text

class Cake2ControllerRedirectMatcher extends \Hamcrest\BaseMatcher { /* snip */ /** * @param Controller $controller * @return bool */ public function matches($controller) { assertThat($controller, is(anInstanceOf(Controller::class))); $headers = $controller->response->header(); if (!isset($headers["Location"])) { return false; } return $controller->response->statusCode() === 302 && $headers[“Location”] === $this->url; } }

Slide 20

Slide 20 text

assertThat(‘foo’, is(intValue())); PHP Fatal error: Uncaught Hamcrest\AssertionError: Expected: is an integer but: was a string "foo"

Slide 21

Slide 21 text

class Cake2ControllerRedirectMatcher extends \Hamcrest\BaseMatcher { /* snip */ public function describeTo(\Hamcrest\Description $description) { $description->appendText("Controller to have status code 302 and a header location containing "); $description->appendValue($this->url); } }

Slide 22

Slide 22 text

class Cake2ControllerRedirectMatcher extends \Hamcrest\BaseMatcher { /* snip */ public function describeMismatch($controller, \Hamcrest\Description $description) { $description->appendText( “Controller response status code:” . $controller->response->statusCode() . “…” ); $headers = $controller->response->header(); if (!isset($headers[‘Location’])) { $this->appendText(“Location header has not been set”); } else { $description->appendText(“Location header is set as: “); $description->appendValue($headers[“Location”]); } } }

Slide 23

Slide 23 text

function redirectedToUrl($url) { return new Cake2ControllerRedirectMatcher($url); } assertThat($controller, redirectedToUrl(“/some/url”));

Slide 24

Slide 24 text

function doSomethingToAString($string) { if (!is_string($string) { throw new InvalidArgumentException(“{$string} is not a string”); } //do something } Not just for tests – reduce boilerplate function doSomethingToAString($string) { assertThat($string, is(stringValue())); //throws \Hamcrest\AssertionError //do something }

Slide 25

Slide 25 text

$array = [‘Hello NomadPHP’, ‘Hello Gareth’, ‘Goodbye :-(‘]; $filtered = array_filter($array, startsWith(‘Hello’)); //[‘Hello NomadPHP’, ‘Hello Gareth’] Matchers as callables

Slide 26

Slide 26 text

//Mockery $mock->shouldReceive(“some_method”) ->with(stringValue()); //Phake Phake::verify($mock)->some_method(stringValue()); Use with mocking libraries Both Phake and Mockery support using Hamcrest matchers for declaring expectations

Slide 27

Slide 27 text

Thank you for listening @garethellis https://github.com/garethellis36/hamcrest-matchers