はじめてのPHPUnit

Blog Single

どうも、チエンといいます。初投稿です。

プログラミングでPHPを使っているけど、テストコードなんて書いたことがないよ!という方のためにPHPUnitの簡単な使い方を紹介したいと思います。

公式ドキュメントはこちら

テストコード自体馴染みが薄いという人にざっくり説明すると、プログラムを作る過程で実際にプログラムを動かして期待通りの動きをするか確かめたり、様々なパターンを試してバグが出ることがないか検証したりすると思いますが、その一連の手順をプログラムとして作成したものがテストコードです。
書くの面倒くさい!て僕含む大体の人は思うかもしれません。ですが面倒さを補ってテストコードはプログラムのクオリティアップ要素、特に品質維持の効率化で大きな役割を果たします。

PHPUnitとは

xUnitという単体テストフレームワーク群があるのですが、それのPHP版がPHPUnitです。専用メソッドなどでテストコードの作成を容易にし、コマンドラインなどからテストを自動で実行したりといったことを可能にします。

単体テスト(ユニットテスト)というのは、プログラムの小さな単位(一般的にはクラスや関数ごと)を個々に動作確認するテストのことを指します。ゆえに単体テストが可能なプログラムを作成したいとなった時に、メソッドが互いに疎結合な見通しの良いプログラムを自然と作りやすいなどといったメリットが見込めます。

早速PHPUnitを導入した簡単なプログラムとそのテストを作成してみましょう。

実践

環境

  • macOS High Sierra v10.13.6
  • PHP v7.1.19
  • PHPUnit v7.4.3

なおインストール方法については公式ドキュメントにもいくつか方法が書いてあるので本記事では割愛させていただきます。サンプルではcomposerを使ってPHPUnitのコマンドをphpunit_sampleディレクトリの/vendor/bin/phpunitにインストールしています。

本体プログラムの用意

まずテスト対象であるクラスと関数を用意します。今回は超シンプルに、渡した数値を二乗した値を返す関数square()を持ったCalculatorクラスをCalculator.phpに作ります。

Calculator.php

<?php

class Calculator
{
    public function square($number)
    {
        return $number ** 2;
    }
}

テストコード作成

このCalculatorクラスをテストするためのコードを書くわけですが、PHPUnitを使ったテストコードの基本的な決まり事として以下があります。

  1. テストクラスの名前はテスト対象のクラス名に「Test」を加えたものにする。
  2. テストクラスは基本的にPHPUnit\Framework\TestCaseを継承する。
  3. テストメソッドはアクセス修飾子をpublicにし、名前の頭に「test」をつけるか、コメント部に@testのアノテーションをつける。
  4. アサーションメソッド(後述)を使って期待している値と実際の値が等しいか判定してテストを行う。

これらを踏まえてテストクラスをCalculatorTest.phpに作ります。

CalculatorTest.php

<?php

use PHPUnit\Framework\TestCase;

require_once 'Calculator.php';

class CalculatorTest extends TestCase
{
    public function testSquare()
    {
        $calc = new Calculator();
        // 7を渡したら49が返ってくることを確認
        $this->assertEquals(49, $calc->square(7));
    }
}

CalculatorTestクラス内にsquare()をテストするtestSquare()を作成しました。
assertEqualsは第1引数(期待値)と第2引数(実際の値)が等しいかどうかを報告するアサーションメソッドです。
ここでは引数に7を渡した場合に返り値がちゃんと49になるか確認をさせています。

テストを実行

コマンドラインから$ ./vendor/bin/phpunit CalculatorTest.phpでテストを実行します。

実行結果(成功)

$ ./vendor/bin/phpunit CalculatorTest.php
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 46 ms, Memory: 4.00MB

OK (1 test, 1 assertion)

実行結果(失敗)

$ ./vendor/bin/phpunit CalculatorTest.php
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 35 ms, Memory: 4.00MB

There was 1 failure:

1) CalculatorTest::testSquare
Failed asserting that 49 matches expected 999.

/Users/chien/workspace/phpunit_sample/CalculatorTest.php:11

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

今回はテストは一つだけですが、このようにテストが成功すると「.」、失敗すると「F」が、テストを一つ実行するたびに表示されます(他にもエラー発生時の「E」などがあります)。失敗した場合は失敗箇所とその原因が続いて表示されます。

補足として、このサンプルでは本体プログラムもテストコードも同じディレクトリ下に置いていますが、規模の大きいプロジェクトなどでファイル数が多い場合は、testsフォルダなどにテストファイル系をひとまとめにして置くのが一般的です。その場合のテストの実行は、$ ./vendor/bin/phpunit testsとフォルダ名を指定してやることでそのフォルダ内の*Test.phpを全て実行することができます。

アサーションメソッド

PHPUnitのアサーションメソッドはassertEquals()の他にも、

  • assertArrayHasKey($key, $array)($keyが$arrayのキーとして存在するか判定)
  • assertCount($expectedCount, $array)($arrayの要素数が$expectedCountに等しいか判定)
  • assertTrue($condition)($conditionが「true」であるか判定)

など、全部で40メソッド以上(とその逆バージョン)用意されており、色々なテストケースに対応できるようになっています。assertEquals()だけでほぼ事足りるような気もしな(

データプロバイダ

さて、分岐のある関数などをテストする際にそれぞれのパターンの動作確認をするために、同じテストを異なる引数で複数回実行したいこともあると思います。その場合アサーションをテストケース分だけ下に随時追加していく書き方でももちろん問題はないのですが、データプロバイダを使用することでコードの短縮を図ることが可能です。
(ちなみにテストが分岐を含めた本体プログラムのコードをどの程度カバーしているか表す割合をカバレッジと呼びます。)
上記のサンプルのtestSquare()で書き表すと、以下のようになります。

    /**
     * @dataProvider squareProvider
     */
    public function testSquare($number, $expected)
    {
        $calc = new Calculator();
        $this->assertEquals($expected, $calc->square($number));
    }

    public function squareProvider()
    {
        return [
         // 'データセット名' => [テストメソッドに渡す引数]
            'Case1' => [7, 49],
            'Case2' => [11, 121],
            'Case3' => [1, 999], // これは失敗する
            'Case4' => [0, 0]
        ];
    }

まずテストメソッドtestSquare()を引数をとって処理を行う形に直します。
そして@dataProviderアノテーションを使ってsquareProviderがテストメソッドtestSquare()データプロバイダメソッドであると指定します。
データプロバイダメソッドは必ずpublicメソッドで、配列の配列を基本返り値とします。

テストを実行してみます。

$ ./vendor/bin/phpunit CalculatorTest.php
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

..F.                                                                4 / 4 (100%)

Time: 59 ms, Memory: 4.00MB

There was 1 failure:

1) CalculatorTest::testSquare with data set "Case3" (1, 999)
Failed asserting that 1 matches expected 999.

/Users/chien/workspace/phpunit_sample/CalculatorTest.php:15

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

squareProvider()から返した配列の数の分だけテストが順に実行されていることが分かります。
このようにPHPUnitが、データプロバイダメソッドの返り値の配列要素を引数としてテストメソッドを要素の分だけ繰り返し実行してくれます。
データセット名になるキーは任意なので省いてもいいのですが、あったほうが大量のテストを実行した際に失敗箇所が分かりやすくなります。

特定のタイミングで実行されるメソッド

setUp()

各テストが走る前に一度ずつ実行されるテンプレートメソッド
用途: テスト用データの用意、インスタンスの生成、プロパティのセットetc…

tearDown()

各テストが走った後に一度ずつ実行されるテンプレートメソッド
用途: 次のテストに影響が出ないよう生成したデータを破棄するetc…

いずれもテストクラス内の全テストメソッドで共通の処理などがあるときに使用します。

例: setUp()の動作確認

<?php

class Calculator
{
    public function square($number)
    {
        return $number ** 2;
    }

    public function cube($number)
    {
        return $number ** 3;
    }
}
<?php 

use PHPUnit\Framework\TestCase;

require_once 'Calculator.php';

class CalculatorTest extends TestCase
{ 
    private $calc;

    // 各テスト実行前の処理
    protected function setUp()
    {
        $this->calc = new Calculator();
    }

    public function testSquare()
    {
        $this->assertEquals(25, $this->calc->square(5));
    }

    public function testCube()
    {
        $this->assertEquals(125, $this->calc->cube(5));
    }

    // 各テスト実行後の処理
    protected function tearDown()
    {
        // 何もしない
    }
}
$ ./vendor/bin/phpunit CalculatorTest.php
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 53 ms, Memory: 4.00MB

OK (2 tests, 2 assertions)

その他のメソッドとして、それぞれ各テストクラスの最初のテスト実行前と最後のテスト実行後に発火するsetUpBeforeClass()tearDownAfterClass()といったものもあります(これらはstaticメソッド)。

最後に

PHPUnitの本当に基本的な部分のみのご紹介となってしまいましたが、テストコードをよく知らなかった方がなんとなくでもどんな感じに書くのかイメージを掴んでいただけたら幸いです。
テストコード書くか書かないかいろんな方針が飛び交っていると思いますが、個人的には的確なテストコードが書ければ、プログラムのデバッグが格段に楽になるし、どうやって効率よくテストカバレッジを100%に届かせるか考えるのは結構楽しいと思います。

参考

Posted by ChienKenichi
趣味はゲーム、映画、休日にたまにひっそりとバイオリン、、、 自己紹介で名前を聞き返される事に定評がある(本人談)

Other Posts: