PHP Unit-Tests ohne Framework

Wer noch nie Tests geschrieben hat fängt oft nicht an weil er denkt er braucht erst ein Framework, eine Ordnerstruktur, einen Test-Runner. Man braucht nichts davon für den Anfang.

Das Prinzip

Ein Test führt Code aus und prüft ob das Ergebnis stimmt.

// test_math.php
function assert_equals(mixed $expected, mixed $actual, string $label): void
{
    if ($expected !== $actual) {
        echo "[FAIL] $label\n";
        echo "       Expected: " . var_export($expected, true) . "\n";
        echo "       Got:      " . var_export($actual, true) . "\n";
    } else {
        echo "[OK]   $label\n";
    }
}

// Code der getestet wird
function add(int $a, int $b): int { return $a + $b; }

// Tests
assert_equals(5,  add(2, 3),  'add(2, 3) = 5');
assert_equals(0,  add(-1, 1), 'add(-1, 1) = 0');
assert_equals(-3, add(-1, -2),'add(-1, -2) = -3');
php test_math.php

Das ist ein Test-Runner. Simpel, direkt, ohne Abhängigkeiten.

Wenn es mehr werden: PHPUnit

composer require --dev phpunit/phpunit
// tests/MathTest.php
use PHPUnit\Framework\TestCase;

class MathTest extends TestCase
{
    public function test_add_positive_numbers(): void
    {
        $this->assertSame(5, add(2, 3));
    }

    public function test_add_negative_numbers(): void
    {
        $this->assertSame(-3, add(-1, -2));
    }
}
vendor/bin/phpunit tests/

Was testen

Nicht alles. Zu viele Tests sind so ein Problem wie zu wenige.

  • Gute Kandidaten:
  • Funktionen mit nicht-trivialer Logik (Berechnungen, Transformationen, Validierung)
  • Code der Randfälle hat (leer, null, negative Zahlen, Unicode)
  • Code der sich oft ändert
  • Nicht sinnvoll:
  • Getter und Setter
  • Framework-Glue-Code
  • Dinge die nur Datenbankzugriff machen

Datenbank-Abhängigkeit isolieren

// Schlecht testbar: direkte PDO-Abhängigkeit
function getUser(int $id): ?array
{
    global $pdo;
    return $pdo->query("SELECT * FROM users WHERE id = $id")->fetch();
}

// Testbar: Dependency Injection
function getUser(PDO $pdo, int $id): ?array
{
    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([$id]);
    return $stmt->fetch() ?: null;
}

Mit Dependency Injection kann man im Test eine SQLite-In-Memory-Datenbank übergeben:

$pdo = new PDO('sqlite::memory:');
$pdo->exec('CREATE TABLE users (id INTEGER, email TEXT)');
$pdo->exec("INSERT INTO users VALUES (1, 'alice@example.com')");

$user = getUser($pdo, 1);
assert($user['email'] === 'alice@example.com');

Data Provider für Parametrisierung

public static function additionProvider(): array
{
    return [
        [2, 3, 5],
        [-1, 1, 0],
        [0, 0, 0],
    ];
}

/** @dataProvider additionProvider */
public function test_add(int $a, int $b, int $expected): void
{
    $this->assertSame($expected, add($a, $b));
}

Ein Test, viele Eingaben — ohne Duplikation.