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);
}
}
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;
}
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);
}
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');
}
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');
}
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)