Le PHP pour les nuls professionnels - PART II

Les Tests

Rémi Alvado / @remialvado / Shopping Adventure

Les principes restent identiques aux autres langages

  1. Ecrire du Code Testable
  2. Respect de la Pyramide des Tests
  3. Faire du Test First

Tests Unitaires

Un seul outil qui domine : PHPUnit

Mise en place :

# composer.json
{
    "name": "remi/test-php",
    "require": {
        "php": ">=5.3.3",
        "phpunit/phpunit": "3.7.*"
    }
}

Lancement :

myhost$ composer update
myhost$ ./bin/phpunit

Cas simple

# Service/Greeting.php
class Greeting {
    function hello($name) {
        return "hello " . $name;
    }
}
# Tests/Service/GreetingTest.php
class GreetingTest extends \PHPUnit_Framework_TestCase {

    /** @test */
    function hello() {
        $greetings = new Greeting();
        $this->assertEquals("hello jonathan", $greetings->hello("jonathan"));
    }
}

Mocks PHPUnit 1/2

# Service/Greeting.php
class Greeting {
    function hello($inputStream) {
        return "hello " . $inputStream->getValue();
    }
}
interface InputStream {
    function getValue();
}
# Tests/Service/GreetingTests.php
class GreetingTest extends \PHPUnit_Framework_TestCase {

    /** @test */
    function hello() {
        $greeting = new Greeting();
        $inputStream = $this->getMock("InputStream");
        $inputStream->expects($this->once())
                    ->method("getValue")
                    ->will($this->returnValue("jonathan"));
        $this->assertEquals("hello jonathan", $greeting->hello($inputStream));
    }
}

Mocks PHPUnit 2/2

# Service/Greeting.php
class Greeting {
    function hello($outputStream, $name) {
        $outputStream->write("hello " . $name);
    }
}
interface OutputStream {
    function write($value);
}
# Tests/Service/GreetingTest.php
class GreetingTest extends \PHPUnit_Framework_TestCase {

    /** @test */
    function hello() {
        $greeting = new Greeting();
        $outputStream = $this->getMock("OutputStream");
        $outputStream->expects($this->once())
                     ->method("write")
                     ->with($this->eq("hello jonathan"));
        $greeting->hello($outputStream, "jonathan");
    }
}

Mocks Mockery

# composer.json
{
    "name": "remi/test-php",
    "require": {
        "php": ">=5.3.3",
        "phpunit/phpunit": "3.7.*",
        "mockery/mockery": "0.8.0"
    }
}
# Tests/Service/GreetingTest.php
class GreetingTest extends \PHPUnit_Framework_TestCase {

    /** @test */
    function hello() {
        $greeting = new Greeting();
        $inputStream = \Mockery::mock('myMockId');
        $inputStream->shouldReceive('getValue')
                    ->times(1)
                    ->andReturn("jonathan");
        $this->assertEquals("hello jonathan", $greeting->hello($inputStream));
    }
}

Matchers Hamcrest

# composer.json
{
    "name": "remi/test-php",
    "require": {
        "php": ">=5.3.3",
        "phpunit/phpunit": "3.7.*",
        "hamcrest/hamcrest": "1.1.0"
    }
}
# Tests/Service/GreetingTest.php
class GreetingTest extends \PHPUnit_Framework_TestCase {

    /** @test */
    function hello() {
        $greeting = new Greeting();
        assertThat($greeting->hello("jonathan"), is("hello jonathan"));
        assertThat($greeting->hello("jonathan"), startsWith("hello"));
        assertThat($greeting->hello("jonathan"), endsWith("jonathan"));
    }
}

Data Provider

# Tests/Service/GreetingTest.php
class GreetingTest extends \PHPUnit_Framework_TestCase {

    function setup() {
        $this->greeting = new Greeting();
    }

    /** 
     * @test
     * @dataProvider getPeopleWithGreetings
     */
    function hello($name, $message) {
        assertThat($this->greeting->hello($name), is($message));
    }

    function getPeopleWithGreetings() {
        return [
            ["jonathan", "hello jonathan"],
            ["victor",   "hello victor"],
        ];
    }
}

Exceptions

# Service/Greeting.php
class Greeting {
    function hello($name) {
        if ($name === "remi") 
            throws new LogicException("hey ! it's me ! silly tool...");
        return "hello " . $name;
    }
}
# Tests/Service/Greetings.php
class GreetingTest extends \PHPUnit_Framework_TestCase {

    /** 
     * @test
     * @expectedException LogicException
     * @expectedExceptionMessage hey ! it's me ! silly tool...
     */
    function helloToSomeoneAlreadySeen() {
        $greeting = new Greeting();
        $greetings->hello("remi");
    }
}

Dependances entre tests

# Tests/Model/StackTest.php
class StackTest extends PHPUnit_Framework_TestCase {
    public function testEmpty() {
        $stack = array();
        $this->assertEmpty($stack);
        return $stack;
    }

    /** @depends testEmpty */
    public function testPush(array $stack) {
        array_push($stack, 'foo');
        $this->assertEquals('foo', $stack[count($stack)-1]);
        $this->assertNotEmpty($stack);
        return $stack;
    }

Tests Fonctionnels

Un outil de BDD reconnu : Behat

Mise en place :

# composer.json
{
    "name": "remi/test-php",
    "require": {
        "php": ">=5.3.3",
        "behat/behat": "2.4.*@stable"
    }
}

Lancement :

myhost$ composer update
myhost$ ./bin/behat

Feature

# features/ls.feature
Scenario: List 2 files in a directory
  Given I am in a directory "test"
  And I have a file named "foo"
  And I have a file named "bar"
  When I run "ls"
  Then I should get:
    """
    bar
    foo
    """

Step

# features/bootstrap/SystemSteps.php
class SystemSteps extends BehatContext {
    /**
     * @Given /^I am in a directory "([^"]*)"$/
     */
    public function iAmInADirectory($dir) {
        if (!file_exists($dir)) {
            mkdir($dir);
        }
        chdir($dir);
    }
}

Assertions

# features/bootstrap/SystemSteps.php
use Behat\Gherkin\Node\PyStringNode;

class SystemSteps extends BehatContext {
    /**
     * @Then /^I should get:$/
     */
    public function iShouldGet(PyStringNode $string) {
        // PHPUnit
        $this->assertEquals($string->getRaw(), $this->output);
        // Hamcrest
        assertThat($string->getRaw(), is($this->output));
        // Custom
        if ($string->getRaw() !== $this->output)
            throws new Exception("value is not expected");
    }
}

Web testing

# composer.json
{
    "name": "remi/test-php",
    "require": {
        "php": ">=5.3.3",
        "behat/behat": "2.4.*@stable",
        "behat/mink":  "1.4@stable"
    }
}

Web crawling

# features/bootstrap/BrowserSteps.php
class BrowserSteps extends BehatContext {
    /**
     * @Given /^I visit the page "([^"]*)"$/
     */
    public function iVisitThePage($url) {
        $driver = new \Behat\Mink\Driver\ZombieDriver();
        $this->session = new \Behat\Mink\Session($driver);
        $this->session->visit($url);
    }

    /**
     * @Given /^I search for "([^"]*)"$/
     */
    public function iSearchFor($query) {
        $page = $this->session->getPage();
        $page->find("#search > input[type=search]")->setValue($query);
        $page->find("#search > input[type=submit]")->press();
    }
}

Navigateurs supportés

  1. Goutte : web scraper
  2. Selenium 1 & 2
  3. Sahi
  4. Zombie : headless browser sur nodejs

Questions ?