TDD in PHP

PHP

Variablen und Typen

// string
$foo = 'bar';

// integer
$foo = 3;

// float
$foo = 3.4;

// array
$foo = [1,2,3];

// assoziatives array
$foo = ['title' => 'Hello', 'size' => 5];

// array < PHP 5.4
$foo = array(1,2,3);

// object
$foo = new MyClass();

// function
$foo = function() { ... };

Strings

// raw
$msg = 'Hello World\r\nHow are you?';

// escaped
$msg = "Hello World\r\nHow are you?";

// escaped mit Variablen
$msg = "Hello $place"; // $place wird ersetzt

// Heredoc 
$msg = <<<EOM
I behave like a double quoted string,
but I can be formatted a lot better.
Even with $variables to be replaced.
EOM;

// Nowdoc
$msg = <<<'EOM'
I behave like a single quoted string.
I can contain reserved steering chars,
like \r or \t without escaping.
$variables are not interpreted.
EOM;

// concat
$msg = 'Hello ' . 'World';

Conditions

// if
if (1 === '1') {
    return 'should never happen';
}

// if else
if (true === false) {
    return 'you\'re drunk!';
}
else {
    return 'do something!';
}

// elseif
if ($color === 'red') {
    //...
}
elseif ($color === 'green') {
    // ...
}
else {
    // ...
}

Switch

$name = 'Bob';
$greeting = null;

switch ($name) {
    case 'Rita':
        $greeting = 'Mrs';
        break;
    case 'Bob':
        $greeting = 'Mr';
        break;
    default:
        $greeting = 'Hi';
}

echo $greeting . ' ' . $name;

Loops

// while
$i = 0;
while ($i < 10) {
    echo $i;
    $i++;
}

// for 
for ($i = 0; $i < 10; $i++) {
    echo $i;
}

// foreach
$arr = [1,2,3];
foreach ($arr as $item) {
    echo $item;
}

// foreach mit keys
$arr = ['foo' => 'bar', 'bar' => 'baz'];
foreach ($arr as $key => $value) {
    echo $key . ' has value: ' . $value;
}

// do while
$i = 0;
do {
    echo $i++;
while ($i < 10);

Superglobals

// enthält Daten aus dem Query String, 
// z.B. http://my.app?foo=bar&bar=1
$_GET['foo'] === 'bar';
$_GET['bar'] === '1'; // STRING!

// enthält Daten aus dem POST Body
$_POST['foo'] === 'bar';

// enthält Infos über den Server, z.B.
$_SERVER['SERVER_NAME'] === 'localhost';
$_SERVER['REQUEST_METHOD'] === 'POST';
$_SERVER['REMOTE_ADDR'] === '<IP Adresse des Besuchers>';

// enthält Dateien, die über multipart/form-data hochgeladen wurden
$_FILES['avatar'] === [
    'name' => 'my-avatar.png',
    'type' => 'image/png',
    'size' => 1234567,
    'tmp_name' => '/tmp/fcajsda.tmp',
    'error' => UPLOAD_ERR_OK,
];

Superglobals

// kann zum speichern von Session Daten verwendet werden.
// Session Daten werden auf dem Server gespeichert.
session_start();
$_SESSION['username'] = 'mario';

// Umgebungsvariablen nutzen...
$_ENV['USER'] === 'www-data';

// Kekse?!
// Cookies werden zum Client geschickt.
$_COOKIE['username'] = 'mario';

// Enthält Daten aus $_GET, $_POST und $_COOKIE
// $_GET < $_POST < $_COOKIE
$_REQUEST['username'] = 'mario';

Klassen

<?php

class Car {
    private $brand; // Wert => null
    protected $type;
    public $nickname = 'unknown';

    public function __construct($brand) {
        $this->brand = $brand;
    }
    
    public function getBrand() {
        return $this->brand;
    }
}

class Bmw extends Car {
    public function __construct() {
        parent::__contruct('BMW');
    }
}

Interfaces

<?php

interface BrandAware {
    public function getBrand();
}

interface GasFillable {
    public function fillGas($liters);
}

class Car implements BrandAware, GasFillable  {
    // ...
    public function getBrand() {
        return 'VW';
    }

    public function fillGas($liters) {
        // ...
    }
}

Konstanten & Enums

<?php

// global möglich...
const NOT_FOUND = 404;

// besser in Klassen
class Human  {
    const NUM_EYES = 2;
    const NUM_HEART = 1;

    // ...
}

// PHP kennt keine Enums, daher...
interface AppState {
    const ACTIVE = 'active',
    const MAINTENANCE = 'maintenance';
    // ...
}

// Benutzung
$myApp->setState(AppState::ACTIVE); // Parametertyp => string

Namespaces

// src/HsBremen/Car/Factory.php
<?php

namespace HsBremen\Car;

class Factory {
    // do something
}

// src/HsBremen/Toy/Factory.php
<?php 

namespace HsBremen\Toy;

class Factory {
    // do something
}

// Benutzung
$carFactory = new \HsBremen\Car\Factory();
$toyFactory = new \HsBremen\Toy\Factory();

// oder
<?php

use HsBremen\Car\Factory;
use HsBremen\Toy\Factory as ToyFactory;

$carFactory = new Factory();
$toyFactory = new ToyFactory();

Exceptions

<?php

class Calculator {
    // ...
    public function divide($a, $b) {
        if ($b === 0) {
            throw new \Exception("You mustn't divide through zero!");
        }
        return $a / $b;
    }
}

// Benutzung
try {
    $calculator = new Calculator();
    $result = $calculator->divide(2, 0);
    echo $result;
}
catch (\Exception $ex) {
    echo $ex->getMessage();
}

Spezielle Exceptions

<?php

class Calculator {
    // ...
    public function divide($a, $b) {
        if (!in_numeric($a) && !is_numeric($b)) {
            throw new \RuntimeException('Operands must be numbers!');
        }
    
        if ($b === 0) {
            throw new \Exception("You mustn't divide by zero!");
        }
        return $a / $b;
    }
}

// Benutzung
try {
    $calculator = new Calculator();
    $result = $calculator->divide(2, 0);
    echo $result;
}
catch (\RuntimeException $ex) {
    echo 'No numbers, try again!';
}
catch (\Exception $ex) {
    echo $ex->getMessage();
}

Eigene Exceptions

<?php

class DivisionByZeroException extends \Exception {
}

class Calculator {
    // ...
    public function divide($a, $b) {    
        if ($b === 0) {
            throw new DivisionByZeroException();
        }
        return $a / $b;
    }
}

// Benutzung
try {
    $calculator = new Calculator();
    $result = $calculator->divide(2, 0);
    echo $result;
}
catch (DivisionByZeroException $ex) {
    echo "You mustn't divide by zero!";
}

Standard PHP Library (SPL)

  • DateTime
  • SplFileInfo
  • SplStack
  • SplQueue
  • SqlFixedArray
  • Exeptions
    • InvalidArgumentException
    • RuntimeException
  • stdClass
  • uvm.

Typisierung

<?php
// Parameter ohne Typ => mixed
public function add($a, $b);

// parameter mit Typ => strict (wie bei Java oder C#)
public function setCreatedAt(\DateTime $date);

// array ist einziger Core-Datentyp (< PHP7) 
public function setMessages(array $messages);

Typisierung in PHP7

<?php
// Parameter mit scalaren Typen (int, bool, string, float)
public function add(int $a, int $b);
public function setDescription(string $desc);

// Rückgabetyp einer Methode definieren
public function getStartDate() : \DateTime;
public function getCount() : int;

// void gibt es weiterhin nicht
public function doSomething(); // nix ": void"!


Wichtige Core Funktionen

<?php

// zähle etwas
count(['foo']);

// leer? 
empty($foo);

// exisitiert und != null
isset($arr['foo']);

// Wert in array (keys werden ignoriert)
in_array($arr);

// Key in array?
array_key_exists('foo', $arr);

Test Driven Development

Test First

  • erst den Test
  • dann die Implementierung
  • schreibe den Test als würde der Code schon existieren

TDD Lifecycle

Rot

Bevor man neuen Produktivcode schreibst, schreibt man einen fehlschlagenden UnitTest der zeigt, was der neue Code leisten soll.

Grün

Unternehme alles Mögliche um den Test grün laufen zu lassen. Wenn du auf Anhieb die komplette Lösung siehst, implementiere sie, wenn nicht, reicht es erstmal den Test grün zu machen.

Refaktor

Überarbeite den Code, Räume auf, finde schöne Möglichkeiten zur Implementierung, Implementiere Logik, etc.

Warum das?

  • Man schreibt nur, was man benötigt
  • Das führt zu einfachem Code
  • Über die Zeit entsteht ein Testgerüst
  • Man hat Sicherheit

Zwei Daumenregeln

  1. Schreibe keinen neuen Produktivcode, bevor du nicht einen neuen, fehlschlagenden Test hast.
  2. Teste alles, was "möglicherweise" Fehlschlagen könnte.

Dauert das nicht viel zu lange?

Muss ich mich stoisch daran halten?

Testtypen

  • Unit Tests
  • Akzeptanz Tests
  • Performance Tests
  • uvm.

Warum Unit Tests?

  • Klein
  • Schnell
  • Automatisierbar

Unit Tests in PHP

wird per Composer installiert

Code

<?php
class Money
{
    private $amount;

    public function __construct($amount)
    {
        $this->amount = $amount;
    }

    public function getAmount()
    {
        return $this->amount;
    }

    public function negate()
    {
        return new Money(-1 * $this->amount);
    }

    // ...
}

Test Code

<?php
class MoneyTest extends PHPUnit_Framework_TestCase
{
    // ...

    public function testCanBeNegated()
    {
        // Arrange
        $a = new Money(1);

        // Act
        $b = $a->negate();

        // Assert
        $this->assertEquals(-1, $b->getAmount());
    }

    // ...
}
  • Klassenname endet auf *Test
  • Erbt von PHPUnit_Framework_TestCase
  • Tests beginnen mit test* oder haben eine @test Annotation
  • $this->assert* stellt Behauptungen auf.

Tests Ausführen

// eine spezifische Datei
php vendor/bin/phpunit --bootstrap vendor/autoload.php tests/MoneyTest

// unser ganzes Projekt
php vendor/bin/phpunit -c phpunit.xml.dist
// Boolsche Aussagen
$this->assertTrue(true);
$this->assertFalse(false);

// Gleichheit (entspricht ===) 
$this->assertEquals(1, 0);
// Gleichheit (an Speicheradresse)
$this->assertSame(new stdClass, new stdClass);

// Leer?
$this->assertEmpty(array('foo'));

// funktioniert mit arrays und strings
$this->assertContains(4, array(1, 2, 3));

// spart die count() Funktion
$this->assertCount(0, array('foo'));

// größer, kleiner
$this->assertGreaterThan(2, 1);
$this->assertGreaterThanOrEqual(2, 1);
$this->assertLessThan(1, 2);
$this->assertLessThanOrEqual(1, 2);
// Key im array?
$this->assertArrayHasKey('foo', array('bar' => 'baz'));

// Typ?
this->assertInstanceOf('RuntimeException', new Exception);

// Null?
$this->assertNull('foo');

Exceptions Testen

<?php

class DivisionByZeroException extends \Exception {}

class Calculator {
    public function divide($a, $b) {    
        if ($b === 0) {
            throw new DivisionByZeroException();
        }
        return $a / $b;
    }
}

class CalculatorTest extends PHPUnit_Framework_TestCase {
    public function testDivisionByZero() {
        // ::class ab PHP 5.5
        $this->expectException(DivisionByZeroException::class); 
        $calc = new Calculator();
        $calc->divide(1,0);
    }

    /**
     * @expectedException DivisionByZeroException
     */
    public function testDivisionByZeroAnnoation() {
        $calc = new Calculator();
        $calc->divide(1,0);
    }
}

SetUp und TearDown

<?php

class CalculatorTest extends PHPUnit_Framework_TestCase {

    /** @var Calculator */
    private $calculator;

    protected function setUp() {
        $this->calculator = new Calculator();
    }

    protected function tearDown() {
        $this->calculator->clearMemory();
    }

    public function testDivision() {
        $this->assertEquals(2, $this->caclulator->divide(10, 5);
    }
}
<?php

class CalculatorTest extends PHPUnit_Framework_TestCase {

    /**
     * @dataProvider provideDataForDivision
     */
    public function testDivision($expected, $a, $b) {
        $this->assertEquals($expected, $this->caclulator->divide($a, $b);
    }

    public function provideDataForDivision() {
        return [
            'first' => [2, 10, 5],
            'next'  => [3, 9, 3],
        ];
    }
}

Mocking

Stubs

<?php

class CalculatorStubTest extends PHPUnit_Framework_TestCase {

    public function testCalculation() {
        $calculator = $this->getMockBuilder(Calculator::class)
                        ->getMock();
        
        $calculator->expects($this->any())
                    ->method('divide')
                    ->willReturn(5);
        
        $this->assertEquals(5, $calculator->divide(1, 2);
    }
}
  • Dummies
  • Fake Rückgabewerte

Mocks

<?php

class CalculatorMockTest extends PHPUnit_Framework_TestCase {

    public function testCalculation() {
        $calculator = $this->getMockBuilder(Calculator::class)
                        ->getMock();
        
        $calculator->expects($this->once())
                    ->method('divide')
                    ->with(10, 5)
                    ->willReturn(2);
        
        $this->assertEquals(2, $calculator->divide(10, 5);
    }
}
  • explizite Methodenaufrufe
  • explizite Parameterwerte

Abhängigkeiten Mocken

<?php

class CalculationTest extends PHPUnit_Framework_TestCase {

    public function testCalculation() {
        $calculator = $this->getMockBuilder(Calculator::class)
                        ->disableOriginalConstructor()
                        ->getMock();

        $calculator->expects($this->once())
                    ->method('divide')
                    ->with(10, 5)
                    ->willReturn(2);

        $math = new Math($calculator);
        
        $this->assertEquals(2, $math->calc("10 / 5");
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         colors="false"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false"
         bootstrap="vendor/autoload.php"
        >

    <testsuites>
        <testsuite name="Web-API Test Suite">
            <directory>./tests</directory>
        </testsuite>
    </testsuites>

    <filter>
        <whitelist>
            <directory>./src</directory>
        </whitelist>
    </filter>

    <logging>
        <log type="coverage-html" target="../coverage" charset="UTF-8" highlight="false" 
            lowUpperBound="35" highLowerBound="70"/>
    </logging>
</phpunit>

Start Testing!

TDD in PHP

By Ole Rößner

TDD in PHP

  • 1,078