CodeBlanks Logo Icon CodeBlanks

Challenge - Writing tests

PHP course · Advanced · Lesson 91/92

Write a test suite for an order service with PHPUnit, mocks and the AAA layout.

Test layout: AAA

In this lesson you learn to write tests. The AAA layout splits each test into three steps: Arrange sets up the test — objects and data; Act invokes the behavior under test; Assert checks that the result matches the expected value.

The starting code provides a minimal TestCase class: assertTrue and assertEqual methods keep $passed and $failed counters. OrderService is the service to test and MockLogger is the stand-in for a dependency. You write OrderServiceTest extends TestCase with testPlaceOrder and testCount methods in AAA layout.

Test doubles and setup

A test double is an object that stands in for a real dependency. In this project MockLogger is a test double: it records log() calls in an array and never writes to a real file or database. Tests stay fast, repeatable and side-effect free.

In the Arrange step of AAA you set up the object under test together with its double. Inside testCount you create an OrderService and a MockLogger; in Act you add an order and call the logger; in Assert you compare $svc->count() and count($logger->calls) with expected values. Each test arranges its own setup; tests stay independent of each other.

Exercise Write the OrderServiceTest class

The starting code has TestCase, OrderService and MockLogger ready. You write the OrderServiceTest extends TestCase class.

Add three methods: run(): void calls testPlaceOrder then testCount. testPlaceOrder(): void follows AAA — Arrange: new OrderService(); Act: placeOrder("Pencil", 3); Assert: $this->assertEqual($order["total"], 30, "total calc"). testCount(): void is also AAA — Arrange: OrderService and MockLogger; Act: placeOrder("Notebook", 1) and $logger->log("order added"); Assert: $svc->count() equals 1 and count($logger->calls) equals 1. Run the demo: $test = new OrderServiceTest(); $test->run(); and print the results.

Show example solution
void { $this->testPlaceOrder(); $this->testCount(); } public function testPlaceOrder(): void { // Arrange $svc = new OrderService(); // Act $order = $svc->placeOrder("Pencil", 3); // Assert $this->assertEqual($order["total"], 30, "total calc"); } public function testCount(): void { // Arrange $svc = new OrderService(); $logger = new MockLogger(); // Act $svc->placeOrder("Notebook", 1); $logger->log("order added"); // Assert $this->assertEqual($svc->count(), 1, "count increased"); $this->assertEqual(count($logger->calls), 1, "log called"); } }. Then run the demo.
Example solution:
<?php
// All parts + test layer: write OrderServiceTest

class ProductRepository {
    private PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
        $this->pdo->exec("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER)");
        $stmt = $this->pdo->prepare("INSERT INTO products (name, price) VALUES (?, ?)");
        $stmt->execute(["Kale", 5]);
        $stmt->execute(["Defter", 15]);
    }

    public function all(): array {
        $rows = $this->pdo->query("SELECT id, name, price FROM products ORDER BY id")->fetchAll(PDO::FETCH_ASSOC);
        return array_map(fn($r) => [
            "id" => (int)$r["id"],
            "name" => $r["name"],
            "price" => (int)$r["price"]
        ], $rows);
    }
}

function handleGet(PDO $pdo): string {
    $repo = new ProductRepository($pdo);
    return json_encode(["products" => $repo->all()], JSON_UNESCAPED_UNICODE);
}

class AuthService {
    private array $users = [];
    private string $csrf = "";

    public function register(string $user, string $pass): void {
        $this->users[$user] = password_hash($pass, PASSWORD_DEFAULT);
    }

    public function login(string $user, string $pass): bool {
        if (!isset($this->users[$user])) return false;
        return password_verify($pass, $this->users[$user]);
    }

    public function csrfToken(): string {
        if ($this->csrf === "") {
            $this->csrf = bin2hex(random_bytes(8));
        }
        return $this->csrf;
    }

    public function applyHeaders(): void {
        header("X-Frame-Options: DENY");
        header("X-Content-Type-Options: nosniff");
    }
}

interface NotificationChannel {
    public function send(string $message): string;
}

class EmailChannel implements NotificationChannel {
    public function send(string $message): string {
        return "Email: " . $message;
    }
}

class SmsChannel implements NotificationChannel {
    public function send(string $message): string {
        return "SMS: " . $message;
    }
}

interface NotificationObserver {
    public function onSent(string $log): void;
}

class LogObserver implements NotificationObserver {
    public array $logs = [];
    public function onSent(string $log): void {
        $this->logs[] = $log;
}

class NotificationFactory {
    public static function make(string $type): NotificationChannel {
        return match ($type) {
            "email" => new EmailChannel(),
            "sms" => new SmsChannel(),
            default => throw new InvalidArgumentException("Bilinmeyen kanal"),
        };
    }
}

class NotificationService {
    private array $observers = [];

    public function attach(NotificationObserver $o): void {
        $this->observers[] = $o;
    }

    public function notify(string $type, string $message): string {
        $channel = NotificationFactory::make($type);
        $result = $channel->send($message);
        foreach ($this->observers as $o) {
            $o->onSent($result);
        }
        return $result;
    }
}

class TestCase {
    public int $passed = 0;
    public int $failed = 0;

    public function assertTrue(bool $cond, string $msg = ""): void {
        if ($cond) { $this->passed++; } else { $this->failed++; echo "FAIL: $msg\n"; }
    }

    public function assertEqual($a, $b, string $msg = ""): void {
        $this->assertTrue($a === $b, $msg);
    }
}

class OrderService {
    private array $orders = [];

    public function placeOrder(string $product, int $qty): array {
        $order = ["product" => $product, "qty" => $qty, "total" => $qty * 10];
        $this->orders[] = $order;
        return $order;
    }

    public function count(): int {
        return count($this->orders);
    }
}

class MockLogger {
    public array $calls = [];
    public function log(string $msg): void {
        $this->calls[] = $msg;
    }
}

class OrderServiceTest extends TestCase {
    public function run(): void {
        $this->testPlaceOrder();
        $this->testCount();
    }

    public function testPlaceOrder(): void {
        // Arrange
        $svc = new OrderService();
        // Act
        $order = $svc->placeOrder("Pencil", 3);
        // Assert
        $this->assertEqual($order["total"], 30, "total calc");
    }

    public function testCount(): void {
        // Arrange
        $svc = new OrderService();
        $logger = new MockLogger();
        // Act
        $svc->placeOrder("Notebook", 1);
        $logger->log("order added");
        // Assert
        $this->assertEqual($svc->count(), 1, "count increased");
        $this->assertEqual(count($logger->calls), 1, "log called");
    }
}

// Demo:
$test = new OrderServiceTest();
$test->run();
echo "Passed: " . $test->passed . "\n";
echo "Failed: " . $test->failed . "\n";

Test yourself

  1. Match test concepts

    assertEqual
    Checks two values are equal
    Mock
    Stand-in for a real dependency
    Arrange
    Setup step before the test
    assertion
    An expectation expression
  2. Why do we use a mock (test double)?

    • To make the code run slower
    • To isolate the test by mimicking the real dependency
    • To write real data to the database
    • To increase the number of errors
    Show answer

    To isolate the test by mimicking the real dependency — Correct. A mock mimics the real dependency's behavior, so the class under test runs in isolation.

  3. Complete the assertion call

    $this->____($order["total"], 30, "total calc");
    Show answer
    $this->assertEqual($order["total"], 30, "total calc");

All lessons in Advanced