Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Write more expressive tests with Hamcrest PHP

Write more expressive tests with Hamcrest PHP

Talk given at the PHPNW 2016 uncon

Cc3abc8d6397214d81c3f5467ff5d3b6?s=128

Gareth Ellis

October 01, 2016
Tweet

Transcript

  1. Write more expressive tests with Hamcrest Gareth Ellis PHPNW 2016

    Uncon
  2. What is Hamcrest?

  3. Matcher library originally written in Java, ported to PHP Allows

    for more expressive assertions in your code
  4. PHPUnit: $this->assertEquals(“2”, $someValue); Hamcrest: assertThat($someValue, is(equalTo(“2”)));

  5. $food = $data_source->getFood(); $fruit = [ 'orange', 'apple', 'pear', ];

    $numberOfFruits = count($fruit); $this->assertEquals(0, count(array_diff($food, $fruit))); $this->assertEquals(0, count(array_diff($fruit, $food))); assertThat( $food, both(containsInAnyOrder($fruit)->and(is(arrayWithSize($numberOfFruits)) ); Real-world example
  6. assertThat(3, is(greaterThan(2))); assertThat($someValue, is(atMost(99))); Number matchers!

  7. assertThat($someString, is(equalToIgnoringCase(“A sTrIng”))); assertThat($someString, is(equalToIgnoringWhiteSpace(“ A string ”))); assertThat($someString, startsWith(“foo”));

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

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

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

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

  12. $ composer require hamcrest/hamcrest-php Usage

  13. $ composer require –dev graze/hamcrest-test-listener With PHPUnit (optional) In phpunit.xml:

    <phpunit> <listeners> <listener class="\Hamcrest\Adapter\PHPUnit\TestListener"></listener> </listeners> </phpunit> Allows PHPUnit to listen for Hamcrest assertions
  14. 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” ] } }
  15. Maximum expressiveness Custom matchers

  16. $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?
  17. $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
  18. class Cake2ControllerRedirectMatcher extends \Hamcrest\BaseMatcher { private $url; public function __construct($url)

    { $this->url = $url; } public function matches($controller) { } public function describeTo(Description $description) { } }
  19. 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; } }
  20. assertThat(‘foo’, is(intValue())); PHP Fatal error: Uncaught Hamcrest\AssertionError: Expected: is an

    integer but: was a string "foo"
  21. 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); } }
  22. 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”]); } } }
  23. function redirectedToUrl($url) { return new Cake2ControllerRedirectMatcher($url); } assertThat($controller, redirectedToUrl(“/some/url”));

  24. 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 }
  25. $array = [‘Hello PHPNW16’, ‘Hello Gareth’, ‘Goodbye :-(‘]; $filtered =

    array_filter($array, startsWith(‘Hello’)); //[‘Hello PHPNW16’, ‘Hello Gareth’] Matchers as callables
  26. Thank you for listening @garethellis https://github.com/garethellis36/hamcrest-matchers