Challenge - Writing tests
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
-
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
-
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.
-
Complete the assertion call
$this->____($order["total"], 30, "total calc");Show answer
$this->assertEqual($order["total"], 30, "total calc");