~ancarda/high-test-coverage

68e8340a88acdc06c07ffd4580e7f4036f73a209 — Mark Dain 3 months ago df05ec9
Implement DateTimeImmutable wrapper
A src/DateTimeImmutable/DateTimeImmutableFactory.php => src/DateTimeImmutable/DateTimeImmutableFactory.php +31 -0
@@ 0,0 1,31 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\DateTimeImmutable;

use DateTimeImmutable;
use DateTimeZone;

/**
 * Factory that produces a DateTimeImmutable instance
 *
 * You should typehint with this interface in all your code. A typical use
 * would be to have a constructor accept an instance like so:
 *
 *     function __construct(DateTimeImmutableFactory $dateTimeImmutable)
 *
 * Which is then used throughout a class. Your Dependency Injection container
 * would then have an entry that resolves to Real:
 *
 *     :DateTimeImmutableFactory:class => Real::class,
 *
 * When that class is under test, you'll instead give it a class like Fixed.
 */
interface DateTimeImmutableFactory
{
    public function __invoke(
        string $datetime = "now",
        ?DateTimeZone $timezone = null
    ): DateTimeImmutable;
}

A src/DateTimeImmutable/Fixed.php => src/DateTimeImmutable/Fixed.php +32 -0
@@ 0,0 1,32 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\DateTimeImmutable;

use DateTimeImmutable;
use DateTimeZone;

/**
 * Return a predetermined fixed value every time
 *
 * This is the simplest possible implementation of DateTimeImmutable.
 * The value given in the constructor is returned from invoke every time.
 */
final class Fixed implements DateTimeImmutableFactory
{
    /** @var DateTimeImmutable */
    private $value;

    public function __construct(DateTimeImmutable $value)
    {
        $this->value = $value;
    }

    public function __invoke(
        string $datetime = "now",
        ?DateTimeZone $timezone = null
    ): DateTimeImmutable {
        return $this->value;
    }
}

A src/DateTimeImmutable/Real.php => src/DateTimeImmutable/Real.php +24 -0
@@ 0,0 1,24 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\DateTimeImmutable;

use DateTimeImmutable;
use DateTimeZone;

/**
 * Produce a real DateTimeImmutable instance
 *
 * This class just wraps DateTimeImmutable::__construct and is intended to be
 * used in production.
 */
final class Real implements DateTimeImmutableFactory
{
    public function __invoke(
        string $datetime = "now",
        ?DateTimeZone $timezone = null
    ): DateTimeImmutable {
        return new DateTimeImmutable($datetime, $timezone);
    }
}

A src/DateTimeImmutable/Succession.php => src/DateTimeImmutable/Succession.php +61 -0
@@ 0,0 1,61 @@
<?php

declare(strict_types=1);

namespace Ancarda\HighTestCoverage\DateTimeImmutable;

use DateTimeImmutable;
use DateTimeZone;
use LogicException;

/**
 * Return the next item in a set of predetermined fixed values every time
 *
 * This implementation takes a list of DateTimeImmutable instances in the
 * constructor. Each time a DateTimeImmutable is requested, the next item in
 * the list is returned and the pointer is moved one place.
 *
 * When the list is exhausted, the pointer wraps around.
 */
final class Succession implements DateTimeImmutableFactory
{
    /** @var array<int, DateTimeImmutable> */
    private $succession = [];

    /** @var int */
    private $cursor = 0;

    /** @var int */
    private $last = 0;

    /**
     * @param array<int, DateTimeImmutable> $succession Non-Empty array
     * @throws LogicException If given an empty array of integers
     */
    public function __construct(array $succession)
    {
        if (count($succession) === 0) {
            throw new LogicException('succession cannot be empty');
        }

        $this->last = count($succession) - 1;
        $this->succession = $succession;
    }

    public function __invoke(
        string $datetime = "now",
        ?DateTimeZone $timezone = null
    ): DateTimeImmutable {
        if ($this->cursor === $this->last) {
            $this->cursor = 0;
            return $this->succession[$this->last];
        }

        return $this->succession[$this->cursor++];
    }

    public function rewind(): void
    {
        $this->cursor = 0;
    }
}

A tests/DateTimeImmutable/FixedTest.php => tests/DateTimeImmutable/FixedTest.php +24 -0
@@ 0,0 1,24 @@
<?php

declare(strict_types=1);

namespace Tests\DateTimeImmutable;

use Ancarda\HighTestCoverage\DateTimeImmutable\Fixed;
use DateTimeZone;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;

final class FixedTest extends TestCase
{
    public function testFixed(): void
    {
        $dt = new DateTimeImmutable();
        $fixed = new Fixed($dt);

        $a = $fixed();
        $b = $fixed();

        self::assertSame($a, $b);
    }
}

A tests/DateTimeImmutable/RealTest.php => tests/DateTimeImmutable/RealTest.php +18 -0
@@ 0,0 1,18 @@
<?php

declare(strict_types=1);

namespace Tests\DateTimeImmutable;

use Ancarda\HighTestCoverage\DateTimeImmutable\Real;
use DateTimeZone;
use PHPUnit\Framework\TestCase;

final class RealTest extends TestCase
{
    public function testReal(): void
    {
        $dt = (new Real())('2000-01-01T00:00:00Z', new DateTimeZone('UTC'));
        self::assertSame('2000-01-01T00:00:00+00:00', $dt->format('c'));
    }
}

A tests/DateTimeImmutable/SuccessionTest.php => tests/DateTimeImmutable/SuccessionTest.php +70 -0
@@ 0,0 1,70 @@
<?php

declare(strict_types=1);

namespace Tests\DateTimeImmutable;

use Ancarda\HighTestCoverage\DateTimeImmutable\Succession;
use DateTimeImmutable;
use DateTimeZone;
use LogicException;
use PHPUnit\Framework\TestCase;

final class SuccessionTest extends TestCase
{
    public function testSuccession(): void
    {
        $a = new DateTimeImmutable('2000-01-01T00:00:00Z');
        $b = new DateTimeImmutable('2001-01-01T00:00:00Z');
        $c = new DateTimeImmutable('2002-01-01T00:00:00Z');
        $d = new DateTimeImmutable('2003-01-01T00:00:00Z');

        $succession = new Succession([$a, $b, $c, $d]);

        self::assertSame($a, $succession());
        self::assertSame($b, $succession());
        self::assertSame($c, $succession());
        self::assertSame($d, $succession());
    }

    public function testSuccessionWrapsAround(): void
    {
        $a = new DateTimeImmutable('2000-01-01T00:00:00Z');
        $b = new DateTimeImmutable('2001-01-01T00:00:00Z');
        $c = new DateTimeImmutable('2002-01-01T00:00:00Z');

        $succession = new Succession([$a, $b, $c]);

        for ($i = 0; $i <= 3; $i++) {
            self::assertSame($a, $succession());
            self::assertSame($b, $succession());
            self::assertSame($c, $succession());
        }
    }

    public function testRewind(): void
    {
        $a = new DateTimeImmutable('2000-01-01T00:00:00Z');
        $b = new DateTimeImmutable('2001-01-01T00:00:00Z');
        $c = new DateTimeImmutable('2002-01-01T00:00:00Z');
        $d = new DateTimeImmutable('2003-01-01T00:00:00Z');

        $succession = new Succession([$a, $b, $c, $d]);

        self::assertSame($a, $succession());
        self::assertSame($b, $succession());
        $succession->rewind();
        self::assertSame($a, $succession());
        self::assertSame($b, $succession());
        self::assertSame($c, $succession());
        self::assertSame($d, $succession());
    }

    public function testSuccessionRejectsEmptyArrays(): void
    {
        $this->expectException(LogicException::class);
        $this->expectExceptionMessage('succession cannot be empty');

        new Succession([]);
    }
}