Lösung: 5

Hierfür legen wir einen neuen Testfall an, damit das Ganze übersichtlich bleibt. Es gibt vier verschiedene Fälle, die wir betrachten müssen, und zwar den Vergleich von Zentimetern miteinander, den Vergleich von Metern miteinander sowie den Vergleich von Metern mit Zentimetern und umgekehrt.

Normalerweise würde ich beim TDD die Testfälle einzeln nacheinander hinschreiben, aber ich will Dir ersparen, Dich durch so viele Lösungen zu klicken. Daher kommen hier alle vier Testfälle auf einmal:

<?php declare(strict_types=1);

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

#[CoversClass(Meter::class)]
#[CoversClass(Centimeter::class)]
class CompareMeasurementsTest extends TestCase
{
    #[Test]
    public function meters_can_be_compared(): void
    {
        $meter = new Meter(1.23);

        $this->assertTrue($meter->equals($meter));
        $this->assertFalse($meter->equals(new Meter(1.24)));
    }

    #[Test]
    public function centimeters_can_be_compared(): void
    {
        $centimeter = new Centimeter(142);

        $this->assertTrue($centimeter->equals($centimeter));
        $this->assertFalse($centimeter->equals(new Centimeter(197)));
    }

    #[Test]
    public function centimeters_can_be_compared_with_meters(): void
    {
        $centimeter = new Centimeter(142);
        $meter = new Meter(1.42);

        $this->assertTrue($centimeter->equals($meter));
        $this->assertFalse($centimeter->equals(new Meter(1.97)));
    }

    #[Test]
    public function meters_can_be_compared_with_centimeters(): void
    {
        $centimeter = new Centimeter(142);
        $meter = new Meter(1.42);

        $this->assertTrue($meter->equals($centimeter));
        $this->assertFalse($meter->equals(new Centimeter(197)));
    }

    #[Test]
    public function exception_when_comparing_centimeter_to_unsupported_measure(): void
    {
        $centimeter = new Centimeter(142);
        $unsupportedMeasurement = $this->createMock(Measurement::class);

        $this->expectException(RuntimeException::class);

        $centimeter->equals($unsupportedMeasurement);
    }

    #[Test]
    public function exception_when_comparing_meter_to_unsupported_measure(): void
    {
        $meter = new Meter(1.42);
        $unsupportedMeasurement = $this->createMock(Measurement::class);

        $this->expectException(RuntimeException::class);

        $meter->equals($unsupportedMeasurement);
    }
}
CompareMeasurementsTest.php

Warte mal, da stehen ja sechs verschiedene Fälle. Das liegt daran, dass wir bisher gewissermaßen Äpfel und Birnen miteinander verglichen haben, denn die Klassen Centimeter und Meter hatten außer struktureller Ähnlichkeit nichts gemeinsam. Für die Signatur einer einheitlichen equals()-Methode brauchen wir aber eine Gemeinsamkeit, deshalb mussten wir das Interface Measurement einführen:

<?php declare(strict_types=1);

interface Measurement
{
    public function equals(Measurement $measurement): bool;
}
Measurement.php

Da wir damit nicht mehr nur auf Centimeter und Meter festgelegt sind, weil ja irgendwer in Zukunft weitere Implementierungen von Measurement erstellen kann, müssen wir uns überlegen, was passiert, wenn wir mit einem solchen neuen Measurement vergleichen. Wir wissen ja noch nicht, wie wir das umrechnen müssen. Falls das jetzt gerade verwirrend war, es wird weiter unten gleich klar.

Erst mal die letzten beiden Testfälle:

#[Test]
public function exception_when_comparing_centimeter_to_unsupported_measure(): void
{
    $centimeter = new Centimeter(142);
    $unsupportedMeasurement = $this->createMock(Measurement::class);

    $this->expectException(RuntimeException::class);

    $centimeter->equals($unsupportedMeasurement);
}

#[Test]
public function exception_when_comparing_meter_to_unsupported_measure(): void
{
    $meter = new Meter(1.42);
    $unsupportedMeasurement = $this->createMock(Measurement::class);

    $this->expectException(RuntimeException::class);

    $meter->equals($unsupportedMeasurement);
}
CompareMeasurementsTest.php

Wir mocken uns in diesen Tests einfach jeweils ein Measurement. Okay, streng genommen sind das Stubs und keine Mocks, aber man ist da im Alltag oft relativ schlampig mit diesen Bezeichnungen. Wichtig ist, dass wir dem Interface Segregation-Prinzip folgen.

So sieht der Vergleich in Meter aus:

public function equals(Measurement $measurement): bool
{
    if ($measurement instanceof Meter) {
        return $this->meters == $measurement->meters;
    }

    if ($measurement instanceof Centimeter) {
        return $this->equals($measurement->asMeter());
    }

    throw new RuntimeException('Unsupported measurement');
}
Meter.php

Wir müssen hier jeweils zwischen den unterstützten Einheiten richtig umrechnen. Wenn Du diese Lösung testgetrieben entwickelt hast, dann hast Du den Rumpf dieser Methode Schritt für Schritt entwickelt, was die Sache vermutlich einfacher gemacht hat, als das jetzt hier aussieht.

Du wunderst Dich vielleicht, warum der Aufruf in Zeile 27 funktioniert, obwohl meters doch privat ist? Das ist eine Besonderheit von PHP. In den neuesten Versionen von PHP könnte man meters auch public readonly deklarieren. Unser Wertobjekt soll ja unveränderlich sein.

Auch in Centimeter müssen wir den Vergleich implementieren:

public function equals(Measurement $measurement): bool
{
    if ($measurement instanceof Centimeter) {
        return $this->centimeters === $measurement->centimeters;
    }

    if ($measurement instanceof Meter) {
        return $this->equals($measurement->asCentimeter());
    }

    throw new RuntimeException('Unsupported measurement');
}
Centimeter.php

Das sieht für Dich wieder wie Code-Duplikation aus? Nein, wenn wir genauer hinsehen, dann sind das in beiden Klassen unterschiedliche Typen, mit denen wir arbeiten und die Umrechnungen sind "andersherum".

Schauen wir mal, ob das alles wie erwartet funktioniert:

PHPUnit 10.2.2 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.14
Configuration: phpunit.xml

......                                                              6 / 6 (100%)

Time: 00:00.003, Memory: 24.41 MB

Compare Measurements
 ✔ Meters can be compared
 ✔ Centimeters can be compared
 ✔ Centimeters can be compared with meters
 ✔ Meters can be compared with centimeters
 ✔ Exception when comparing centimeter to unsupported measure
 ✔ Exception when comparing meter to unsupported measure

OK (6 tests, 10 assertions)
Ausgabe von phpunit --filter CompareMeasurementsTest --testdox
phpunit --filter CompareMeasurementsTest --testdox ausführen