もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

PHPUnit + Phake

はじめに

こんばんは。今回も積本消化月間 vol.1で手を進みながら勉強した、PHPUnit + Phakeのログ的なものをきれいに整形してブログにしました。

phpは普段業務で使っているので、ハマるところはなくスムーズに進みました。

今回も前回同様やってみたログなので、プログラムコードに関しては、本で紹介されている内容をほぼほぼ再現しています。

本題

assert()を使用したテスト

phpunitなどを使わず、assert()を使用した場合のテスト方法を紹介されています。

# sum.php
<?php

function sum(int $a, int $b): int
{
    return $a + $b;
}

# test_sum.php
<?php

require_once 'sum.php';

$result = sum(1, 2);
assert($result === 4, '1 + 2は3を返す');

自分の環境では、普通に実行しただけでは何も表示されなかったのですが、本書で紹介されている、 assert.exception = 1zend.assertions = 1 を指定することで、狙い通り例外が発生しました。

php -d assert.exception=1 -d zend.assertions=1 test_sum.php 
PHP Fatal error:  Uncaught AssertionError: 1 + 23を返す in /path/to/phpunit_phake/test_sum.php:6
Stack trace:
#0 /path/to/phpunit_phake/test_sum.php(6): assert(false, '1 + 2\xE3\x81\xAF3\xE3\x82\x92\xE8\xBF\x94...')
#1 {main}
  thrown in /path/to/phpunit_phake/test_sum.php on line 6

phpカンファレンス@t_wada さんが話していた、 表明プログラミングの部分で登場していた assert()関数でした。

https://www.youtube.com/watch?v=17i1EL9pBwA

youtubeでも紹介されているのですが、 zend.assertions-1 にしてあげれば、assert() のコードは生成されない動作になります。

なので、開発時に assert() をテストとして使用して、本番では -1 にしてあげてassert()が生成されない動作にしてあげることが出来ます。

自分が考えるこれの旨味は、testファイルとして分けなくても、実際に使用されるクラスなり関数なりに直接書くことができるというところですが、いくら運用時に生成されないとはいえ、assert() のコード自体は存在しているので、ちょっとみにくくなるなと思いました。

assert()を「テストツール」として使用するのではなく、youtubeでも紹介されている通り、「暗黙の前提を表明するツール」と割り切って、別でテストツールを使うのが現実的だと思いました。

phptを使ったテスト

これは自分知らなかったのですが、phpの標準テストツールの phpt を使ったテストが紹介されていました。

ただ、こちらはpearのインストールが必要とのことでした。

おそらくですが、今後pearを使うことはないと思うので、これに関しては読み飛ばしました。

PHPUnitを使ったテスト

自分毎回お世話になっている PHPUnitを使用したテストです。

インストール等はググれば死ぬほど乗ってるので、省略します。

# Sum.php
<?php

namespace Kojirock;

class Sum
{
    public static function sum(int $a, int $b): int
    {
        return $a + $b;
    }
}

# SumTest.php
<?php

use Kojirock\Sum;

class SumTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @test
     */
    public function 2つの値を足し算できること()
    {
        $this->assertSame(4, Sum::sum(1, 2));
    }
}
$ php vendor/bin/phpunit tests
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 54 ms, Memory: 4.00 MB

There was 1 failure:

1) SumTest::2つの値を足し算できること
Failed asserting that 3 is identical to 4.

/path/to/tests/SumTest.php:12

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

よく見る結果が出てきました。

本書では、よく使われる assertメソッドが紹介されていました。

知らないものが多かったというか、自分は正直、assertSame()assertTrue()|assertFalse()expectException() expectExceptionMessage または、 expectExceptionObject を使うくらいなので、その他はほぼ使いません。。。

フィクスチャを使用する

# TaxCalculator.php
<?php
namespace Kojirock;
use DateTimeInterface;
use DateTime;

class TaxCalculator
{
    private $tax3perDate;
    private $tax5perDate;
    private $tax8perDate;
    private $tax10perDate;

    public function __construct()
    {
        $this->tax3perDate = new DateTime('1989-04-01');
        $this->tax5perDate = new DateTime('1997-04-01');
        $this->tax8perDate = new DateTime('2014-04-01');
        $this->tax10perDate = new DateTime('2019-04-01');
    }

    public function calc(int $amount, DateTimeInterface $current):int
    {
        if ($this->tax3perDate <= $current && $current < $this->tax5perDate) {
            return $this->multiple($amount, 103);
        } elseif ($this->tax5perDate <= $current && $current < $this->tax8perDate) {
            return $this->multiple($amount, 105);
        } elseif ($this->tax8perDate <= $current && $current < $this->tax10perDate) {
            return $this->multiple($amount, 108);
        } elseif ($this->tax10perDate <= $current) {
            return $this->multiple($amount, 110);
        }

        return $amount;
    }

    private function multiple(int $amount, int $hundredTimesTaxRate): int
    {
        $multi = $amount * $hundredTimesTaxRate;

        return (int)floor($multi / 100);
    }
}

# TaxCalculatorTest.php
<?php

use Kojirock\TaxCalculator;

class TaxCalculatorTest extends \PHPUnit\Framework\TestCase
{
    private $taxCalculator;

    public function setUp()
    {
        $this->taxCalculator = new TaxCalculator();
    }

    /**
     * @test
     */
    public function 税率5パーセントの金額を計算できる()
    {
        $date = new DateTime('2014-03-31');
        $this->assertSame(105, $this->taxCalculator->calc(100, $date));
    }
    /**
     * @test
     */
    public function 税率8パーセントの金額を計算できる()
    {
        $date = new DateTime('2018-10-31');
        $this->assertSame(108, $this->taxCalculator->calc(100, $date));
    }
    /**
     * @test
     */
    public function 税率10パーセントの金額を計算できる()
    {
        $date = new DateTime('2019-10-01');
        $this->assertSame(110, $this->taxCalculator->calc(100, $date));
    }
}
$ php vendor/bin/phpunit tests
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 39 ms, Memory: 4.00 MB

OK (3 tests, 3 assertions)

こちらもよく使用する setUp の紹介でした。

Phakeを使ったテスト

Mockeryとかは Laravelプロジェクトで少々触ったことがあったのですが、Phakeは使ったことありませんでした。

PHPUnit標準で搭載されているモックツールよりも使い勝手が良いとのことで、試してみます。

インストール

$ composer require --dev phake/phake:3.1.3
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing phake/phake (v3.1.3): Downloading (100%)         
phake/phake suggests installing doctrine/common (Allows mock annotations to use import statements for classes.)
phake/phake suggests installing hamcrest/hamcrest-php (Use Hamcrest matchers.)
Writing lock file
Generating autoload files
——

実装コード

# CostReport.php
<?php

namespace Kojirock;

class CostReport
{
    private $repository;

    private $taxCalculator;

    public function __construct(CostRepository $repository, TaxCalculator $taxCalculator)
    {
        $this->repository = $repository;
        $this->taxCalculator = $taxCalculator;
    }

    public function report(): array
    {
        $rows = $this->repository->findAll();
        return array_map(function (array $row) {
            return $this->taxCalculator->calc($row['value'], new \DateTime($row['date']));
        }, $rows);
    }
}

# CostRepository.php
<?php
namespace Kojirock;

class CostRepository
{
    private $pdo;

    public function __construct(\PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function findAll()
    {
        $stmt = $this->pdo->query('SELECT * FROM report_table');
        $stmt->execute();
        return $stmt->findAll();
    }
}

# CostReportTest.php
<?php

use Kojirock\TaxCalculator;
use Kojirock\CostReport;
use Kojirock\CostRepository;


class CostReportTest extends \PHPUnit\Framework\TestCase
{
    private $costReport;
    private $mockRepository;

    public function setUp()
    {
        $this->mockRepository = Phake::mock(CostRepository::class);
        $this->costReport     = new CostReport($this->mockRepository, new TaxCalculator());
    }

    /**
     * @test
     */
    public function 税率8パーセントのコストをレポートできる()
    {
        Phake::when($this->mockRepository)
            ->findAll()
            ->thenReturn([[
                'date' => '2018-10-31',
                'value' => 100
            ]]);

        $this->assertSame([108], $this->costReport->report());
    }
}
php vendor/bin/phpunit tests
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 59 ms, Memory: 4.00 MB

OK (1 test, 1 assertion)

PDOを使用してDBに接続するCostRepositoryというクラスをmockしてテストできるようにしました。

自分が使用したことのある Mockery と直感的にあまり変わりませんでした。

だいたいmockを使用するのって外部API連携などの部分かと思います。

終わりに

最近仕事がマジで忙しくて、ブログはもちろん、1月1回チャレンジ、毎週運動が全然出来てない状況です。

とりあえずGWすぎれば落ち着くはずなので、それまでは簡単なブログのみになっちゃいそうです。。

現場からは以上です。