Solution: 2
PHPUnit 10.2.2 by Sebastian Bergmann and contributors. Runtime: PHP 8.2.14 Configuration: phpunit.xml .... 4 / 4 (100%) Time: 00:00.001, Memory: 24.41 MB Measurements ✔ Centimeters can be converted to int ✔ Centimeters cannot be negative ✔ Meters can be converted to float ✔ Meters cannot be negative OK (4 tests, 4 assertions)
We first write a test for centimeters
that expects an exception if the passed length dimension is negative. To keep the example compact, we do not use our own exception, but the RuntimeException
built into PHP.
#[Test]
public function centimeters_cannot_be_negative(): void
{
$this->expectException(RuntimeException::class);
new Centimeter(-1);
}
This test still fails because the expected exception is not thrown.
Therefore, we now add the call of a corresponding ensure method
in the constructor:
public function __construct(int $centimeters)
{
$this->ensureNotNegative($centimeters);
$this->centimeters = $centimeters;
}
This method is easy to implement:
private function ensureNotNegative(int $centimeters): void
{
if ($centimeters < 0) {
throw new RuntimeException('Measure cannot be negative');
}
}
Our test now turns green. This completes the first part.
We continue with meters
and proceed in the same way:
#[Test]
public function meters_cannot_be_negative(): void
{
$this->expectException(RuntimeException::class);
new Meter(-1);
}
We insert the corresponding code into Meter
:
<?php declare(strict_types=1);
class Meter
{
private readonly float $meters;
public function __construct(float $meters)
{
$this->ensureNotNegative($meters);
$this->meters = $meters;
}
public function asFloat(): float
{
return $this->meters;
}
private function ensureNotNegative(float $meters): void
{
if ($meters < 0) {
throw new RuntimeException('Measure cannot be negative');
}
}
}
Have we now created code duplication? Should we introduce a common abstract base class for Centimeter
and Meter
?
What looks like duplication at first glance is actually just structurally similar code. Let's take a closer look: we work once with int
and once with float
. To standardize this into one method, we would have to work with typecasts and convert the integers to floating point numbers. This would cause problems with the accuracy of the calculation, which we should avoid as far as possible.
The only line that is actually identical in both classes is the throwing of the exception. However, this is only the case because we use a relatively generic exception message. If we were talking specifically about centimetres or meters here, this line would no longer be identical in both classes.
The solution at a glance:
<?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 MeasurementsTest extends TestCase
{
#[Test]
public function centimeters_can_be_converted_to_int(): void
{
$centimeters = 23;
$measure = new Centimeter($centimeters);
$this->assertEquals($centimeters, $measure->asInt());
}
#[Test]
public function centimeters_cannot_be_negative(): void
{
$this->expectException(RuntimeException::class);
new Centimeter(-1);
}
#[Test]
public function meters_can_be_converted_to_float(): void
{
$meters = 1.5;
$measure = new Meter($meters);
$this->assertEquals($meters, $measure->asFloat());
}
#[Test]
public function meters_cannot_be_negative(): void
{
$this->expectException(RuntimeException::class);
new Meter(-1);
}
}
<?php declare(strict_types=1);
class Centimeter
{
private readonly int $centimeters;
public function __construct(int $centimeters)
{
$this->ensureNotNegative($centimeters);
$this->centimeters = $centimeters;
}
public function asInt(): int
{
return $this->centimeters;
}
private function ensureNotNegative(int $centimeters): void
{
if ($centimeters < 0) {
throw new RuntimeException('Measure cannot be negative');
}
}
}
<?php declare(strict_types=1);
class Meter
{
private readonly float $meters;
public function __construct(float $meters)
{
$this->ensureNotNegative($meters);
$this->meters = $meters;
}
public function asFloat(): float
{
return $this->meters;
}
private function ensureNotNegative(float $meters): void
{
if ($meters < 0) {
throw new RuntimeException('Measure cannot be negative');
}
}
}