RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

PHPUnit テストコードの書き方【入門】

はじめに

こんにちは、MasaKu です。

ソースコードの修正によりこれまで保証していた動作が保証されず不具合になってしまうケースがあるかと思います。

こういった不具合を減らすためにも手動によるテストに加えて、テストコードによる繰り返しテストを実行できるようにしておくとも重要です。

PHPでは PHPUnit というユニットテストツールを利用することでテストコードを作成することができます。

今回はPHPで作成されたさいころプログラムを例にして PHPUnit のテストコードの書き方をご紹介いたします。

なお、弊社のエンジニアブログにてPHPUnit で利用するアサーションメソッドについて解説された記事がございますので、こちらもあわせてご確認いただけますと幸いです。

PHPUnitのアサーションメソッドを知ろう! - RAKUS Developers Blog | ラクス エンジニアブログ

PHPUnit の基本

PHPUnit とは PHP コードで記述可能なユニットテストツールです。

そのため、普段から PHP のコードを書くプログラマにはとても親しみやすいと思います。

PHPUnit では テストコードを記述するテストクラスを作成し、そのクラス内で実施したいテストメソッドを追加していくというのが大まかな流れです。

テストメソッドをどのように作成するかがテストコードを作成する上での重要なポイントかと思いますが、流れとしては以下になります。

  1. アサーションによるテストケースの作成
  2. アノテーションによるテストケースの依存性定義

単純なテストであれば、対象のプログラムのふるまいを確認したいアサーションを実行するテストメソッドを実装するだけでユニットテストが作成できます。

以下では PHP で作成されたさいころプログラムのテストコードを例にして PHPUnit によるテストコード作成の流れを解説したいと思います。

テスト対象のプログラム

今回作成したさいころプログラムは以下のような構成になっています。

  • さいころクラス
    • 6面体である
    • 1から6までの出目を持っている
    • 転がすことで出目が確定する
    • 出目を確認することができる

さいころクラスの詳細

<?php

// さいころクラス
class Dice {
    protected array $sided;
    protected int $number;

    public function __construct (){
    }

    public function setSided() :void{
        $this->sided = [1,2,3,4,5,6];
    }

    public function getSided() :array{
        return $this->sided;
    }

    public function roll() :void{
        $this->number = $this->sided[array_rand($this->sided)];
    }

    public function getNumber() :int{
        return $this->number;
    }
}

PHPUnit によるテストケース作成

以下では上記のプログラムのテストコードを解説していきます。

なお、PHPUnit のバージョンは 9.5.6 として記載していきます。

まずはざっくりとプログラムを確認していただければと思います。

さいころクラスのテストケース

<?php

class DiceTest extends TestCase
{
    protected Dice $dice;

    protected function setUp() :void{
        $this->dice = new Dice();
    }

    public function testInstanceOf() {
        $this->assertInstanceOf(Dice::class, $this->dice);
    }

    public function testEmpty(){
        $this->assertTrue(empty($this->dice->sided));
    }

    public function testSided(){
        $this->dice->setSided();
        $this->assertCount(6, $this->dice->getSided());
        $this->assertContains(1, $this->dice->getSided());
        $this->assertContains(2, $this->dice->getSided());
        $this->assertContains(3, $this->dice->getSided());
        $this->assertContains(4, $this->dice->getSided());
        $this->assertContains(5, $this->dice->getSided());
        $this->assertContains(6, $this->dice->getSided());

        return $this->dice;
    }

    /**
     * @depends testSided
     */
    public function testRoll($dice){
        $dice->roll();
        $this->assertTrue(1 <= $dice->getNumber() && 6 >= $dice->getNumber());
    }
}

テストクラスの作成方法

まず、テストクラスを作成する方法について解説していきます。

PHPUnit でテストクラスを作成する際にはいくつかルールがあります。

こちら、PHPUnit の公式を参照させていただきます。

  1. Class という名前のクラスのテストは、ClassTest という名前のクラスに記述する
  2. ClassTest は、(ほとんどの場合) PHPUnit\Framework\TestCase を継承する

今回のケースだと、Dice というさいころクラスがテスト対象であるため、テストクラスの名称は DiceTest という名前になります。

また、2番目のルールとしてPHPUnit\Framework\TestCaseを継承する、というものがあります。これは、PHPUnitでのテスト実行時に必要となる各種メソッドをテストクラス内で利用するために親クラスであるTestCaseから継承したいためです。

(ほとんどの場合) という注意書きがありますが、TestCase クラスを継承したテストクラスを継承してテストケースを作成したい、などのシーン以外では、基本的には TestCase を継承してテストクラスを作成する方法で問題ないと思います。

テストメソッドの作成方法

テストメソッドの作成についても PHPUnit の公式側でルールが決められています。

  • テストは、test* という名前のパブリックメソッドとなります。
    • あるいは、@test アノテーション をメソッドのコメント部で使用することで、それがテストメソッドであることを示すこともできます。

アノテーションを付けることも許可されていますが、個人的にはメソッド名の先頭に test を付けるルールに従う方針で良いのではないかと思います。

この後にも登場しますが、アノテーションはテストコードのメタデータを表す情報となるため、できる限りノイズになるものは少なくした方が良いのではないかと思います。 (全メソッドに @test というアノテーションを付けるのも無駄かと思います)

テストケースの解説

それでは、テストコードについて解説していきます。

作成したテストコードは以下の通りです

  1. テスト実施前の準備
  2. テスト対象がDiceクラスのインスタンスであるかの確認
  3. さいころの目が意図せず確定していないことの確認
  4. さいころの目が6面体であり1から6までの出目を持っていることの確認
  5. さいころを振れば1から6までのいずれかの出目がでることの確認

それぞれについて詳しく解説していきます

setUpメソッド

setUp() では、PHPUnit にてテストコードを実行する際にはじめに実行されるメソッドです。

今回の例ではテスト対象となる Dice というクラスのオブジェクトを生成する処理を記載しました。

ここで生成されたオブジェクトを用いて以降のテストケースを実行していきます。

こちらについての詳しい内容は以下のページをご参照ください。

phpunit.readthedocs.io

アサーションメソッド

ここから実際にテスト対象のプログラムが、期待通りのふるまいで実装されているかを確認するためのテストコードについて解説していきます。

おさらいになりますが、今回のテスト対象となる Dice クラスは以下の通りです。

  • Diceクラスの特徴
    • 6面体である
    • 1から6までの出目を持っている
    • 転がすことで出目が確定する
    • 出目を確認することができる

これらの特徴が必ず保証されていることを確認するためのテストケースを作成していきます。

assertInstanceOf()

assertInstanceOf() は対象となるオブジェクトが指定されたクラスのオブジェクトかどうかを判定するアサーションメソッドです。

setUp() によりDice クラスのオブジェクトを生成しているため問題ないと思いますが、オブジェクトが期待通りのものになっていることを確認するようにします。

assertInstanceOf() の注意点なのですが、指定するオブジェクトのサブクラスのオブジェクトであってもテスト成功と判定されてしまいます。

つまり、Dice クラスを継承した DummyDice クラスなどを作成し assertInstanceOf() を実行した場合、テスト成功となってしまうというわけです。

この動きは PHP の標準機能の instanceOf という型演算子と同様の動きですのでご注意ください。

www.php.net

assertTrue()

assertTrue() はTrueが返されることを確認するアサーションメソッドです。

Dice クラスは出目をセットするメソッド(setSideメソッド)を実行するまではどのような出目を持っているかが確定しない仕様になっていますので、PHPの標準関数 empty() を実行することで、設定されているかを検証することができます。

ちなみに、配列が空であるかどうかをチェックするだけであれば assertEmpty() というアサーションメソッドも存在します。

しかし、PHP標準関数の empty() では変数が定義されているのか、というところまでチェックできます。

www.php.net

同じアサーションメソッドでもテストコードの記述を工夫することで柔軟にテストケースが書けるのもいいですね。

<?php

// 同じ意味のテストケース
public function testEmpty(){
    // 以下は成功になる
    $emptyArray = [];
    $this->assertTrue(empty($emptyArray));
    $this->assertEmpty($emptyArray);

    // 以下は assertEmpty が失敗になる
    $this->assertTrue(empty($notSetArray));
    $this->assertEmpty($notSetArray);
}

以下は上記のテスト実行結果です。

PHPUnit 9.5.6 by Sebastian Bergmann and contributors.

E                                                                   1 / 1 (100%)

Time: 00:00.010, Memory: 4.00 MB

There was 1 error:

1) PhpUnitTest::testEmpty
Undefined variable $notSetArray

/home/masaku/study/phpunit/test/PhpUnitTest.php:15

ERRORS!
Tests: 1, Assertions: 3, Errors: 1.

assertCount()

assertCount() は指定された配列の要素数が期待通りかどうかを確認するアサーションメソッドです。

こちらも assertSame() というアサーションメソッドを利用することで、同様のテストコードを記述することができます。

<?php

// 同じ意味のテストケース
public function testSided(){
    $this->dice->setSided();
    $this->assertCount(6, $this->dice->getSided());
    $this->assertSame(6, count($this->dice->sided()));
}

assertSame() を利用しても同じ意味のテストコードが記述できますが、配列の要素数のテストに関しては assertCount() を利用した方が、テスト失敗時のエラーメッセージがより分かりやすくなる、というメリットがあります。

// asertCount() で失敗した場合のメッセージ
1) DiceTest::testSided
Failed asserting that actual size 6 matches expected size 5.

// assertSame() で失敗した場合のメッセージ
1) DiceTest::testSided
Failed asserting that 6 is identical to 5.

PHPUnit ではアサーションメソッドが豊富ですので、狙い通りのアサーションメソッドが見つかる場合もあるかと思います。しかし、標準関数を併用することで期待するテストコードが記述できますので、あまり見かけないアサーションメソッドを利用してテストコードの可読性を下げてしまうよりは汎用的なアサーションメソッドから実行できるようにする、ということも一定メリットがあるかと思います。

assertContains()

assertContains() は配列内に指定した値を持つ要素が含まれているかを確認するアサーションメソッドです。

少しわかりにくくなってしまいますが、こちらも PHP 標準関数の array_search を利用すれば assertSame() と組み合わせることで同様のテストケースが作成可能です。

<?php

public function testSided(){
    $this->dice->setSided();
    $this->assertContains(1, $this->dice->getSided());
    $this->assertSame(0, array_search(1, $this->dice->getSided()));
}

このテストケースにより 1~6 の数値を持っていることが確認できました。

アノテーション

最後に PHPUnit でのテストコード作成する際、より複雑なテストコードを作成する上で重要になるアノテーションについて解説いたします。

アノテーションとはテストメソッドに対するメタデータを表す構文のことで、PHPDoc などでも利用されています。

PHPUnit ではテストケースの依存性を表現したり、各テストの実行後に毎回実行して欲しい処理などを表現するために利用します。

@depends

@depends というアノテーション はテストケースの依存性を表すアノテーションです。

<?php
public function testSided(){
    // 省略
    return $this->dice;
}

/**
 * @depends testSided
 */
public function testRoll($dice){
    $dice->roll();
    $this->assertTrue(1 <= $dice->getNumber() && 6 >= $dice->getNumber());
}

testRoll というさいころを振った際の数値が1から6の間で出現するかどうかを確認するテストケースですが、こちらのテストケースの前提は 1~6 の数値を持った6面体のさいころであるということが前提になっています。

そのため、上記観点のテストコードを通過できた Dice クラスのオブジェクトでテストを実施する必要があります。
(6の目しか出ないさいころでもテストが合格になってしまうため)

このような、その他のテストケースの実行後の戻り値を受け取ってテストを実施したい場合はアノテーションを以下のように記述します。

@depends + "テストメソッド名"

このように記述することで、アノテーションが付与されたテストメソッド側で依存している戻り値を引数として受け取ることができます。

これで期待通りのテストケースが実行できます。

テストコードの実行結果

それでは、上記テストをPHPUnitで実行した結果を確認したいと思います。

PHPUnit 9.5.6 by Sebastian Bergmann and contributors.

....                                                                4 / 4 (100%)

Time: 00:00.010, Memory: 4.00 MB

OK (4 tests, 10 assertions)

このように全部で4個のテストメソッド(10個のアサーション)がすべて成功になりました。

もし、テスト対象としている Dice クラスの出目が全て6のさいころなどに書き換わってしまった場合などは、PHPUnit 側でエラーを検知することができます。

PHPUnit 9.5.6 by Sebastian Bergmann and contributors.

..FS                                                                4 / 4 (100%)

Time: 00:00.030, Memory: 4.00 MB

There was 1 failure:

1) DiceTest::testSided
Failed asserting that an array contains 1.

/home/masaki/study/phpunit/test/DiceTest.php:30

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

上記の通り、サイコロの出目に1が含まれていないことが確認できています。

おわりに

いかがでしたでしょうか。

PHPUnit はテストコードそのものも PHP のコードで記述できるため、PHPの開発をしている開発者であれば簡単にテストコードが記述できるかと思います。

テストコードが書きたいプログラム自体がテストコードが書きやすい構成になっているか、という課題はありますが、テストコードを書く習慣が無いという方でも導入のイメージが持てるようになっていれば幸いです。

このときはこういう動きをする ということがある程度固定化されるクラスやメソッドの場合はテストコードを記述しておくことで、コード修正時の不具合を未然に防ぐことができますので、積極的にテストコードを書いていきたいと思います。

参考URL

PHPUnit マニュアル — PHPUnit latest Manual

PHP: Hypertext Preprocessor

PHPUnitのアサーションメソッドを知ろう! - RAKUS Developers Blog | ラクス エンジニアブログ


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

Copyright © RAKUS Co., Ltd. All rights reserved.