PHPUnitはじめの一歩 第一歩環境構築とシンプルなテスト作成の話

今回からPHPのxUnitであるPHPUnitのご紹介をしたいと思います。

xUnitとは?

ケント・ベックが作成したSUnitが元で作られたテスティングフレームワークのことです…
xUnitのxの部分は、言語を指しておりSmalltalkだったら、SUnit , JavaならJUnit , PHPならPHPUnitと言う感じになります

と、まじめな話はそんだけです。

PHPUnitとは!?

略 : https://phpunit.readthedocs.io/ja/latest/index.html

イントロダクションの話

私がxUnitに出会ったのはJUnitで… (略)
PHPUnitと出会ったのは、Laravelでの開発からである。
その環境を一番シンプルに作るには?と考えはじめたのが今回の記事です
要するに、Laravelまで入れ込んでテストにしたら、スゴくづらいですし、CakePHPでは?レガシーには?
そんな感じになるわけで….
そこで超シンプルな感じにする!

と言っても

phpunit Testクラス.php

では、意味がない。

プロジェクトのディレクトリ構成

格好良く、以下のディレクトリ環境を想定します
src : ソースが入っています
tests : テストが入っています

テストには以下の2種類を作ることとします。
あくまでも、私流なのを注意しておきます

Feature :
  機能をテストする
Unit :
  メソッドをテストする

この違いを説明するのは難しいですが、Laravelを考えると理解しやすいです
Feature :
  urlにアクセスして、結果を評価する
Unit :
  Classファイルを読み取りメソッドを試験する

んな感じです。
Feature :
  /にgetをcallしたらstatus 200を返すかの確認
  /notにgetをcallしたらstatus 404を返すかの確認
Unit :
  Service.classのserveクラスの引数’hello’を渡せば、’Hello World’を返してくれるかの確認

ざっくりと構成を書くとそんな感じです

プロジェクト設定の定義する

今回の空っぽのプロジェクトをGitHubに上げております、ご参考までに

https://github.com/wataru775/learning-phpunit/tree/v0.1

はじめに、私の環境を書いておきます

$ php -v
WARNING: PHP is not recommended
PHP is included in macOS for compatibility with legacy software.
Future versions of macOS will not include PHP.
PHP 7.3.24-(to be removed in future macOS) (cli) (built: Feb 28 2021 09:53:14) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.24, Copyright (c) 1998-2018 Zend Technologies

$ composer -V
Composer version 2.0.13 2021-04-27 13:11:08

次に、実際に実行できる環境を作りましょう!
ディレクトリの作成方法は… 無視!

プロジェクトディレクトリに移動して

$ composer init

                                            
  Welcome to the Composer config generator  
                                            


This command will guide you through creating your composer.json config.

Package name (<vendor>/<name>) [wataru/pphpunit]: 
Description []: 
Author [wataru <wataru775@gmail.com>, n to skip]: 
Minimum Stability []: 
Package Type (e.g. library, project, metapackage, composer-plugin) []: project
License []: 

Define your dependencies.

Would you like to define your dependencies (require) interactively [yes]? 
Search for a package: 
Would you like to define your dev dependencies (require-dev) interactively [yes]? 
Search for a package: phpunit/phpunit
Enter the version constraint to require (or leave blank to use the latest version): 
Using version ^9.5 for phpunit/phpunit
Search for a package: 

{
    "name": "wataru/pphpunit",
    "type": "project",
    "require-dev": {
        "phpunit/phpunit": "^9.5"
    },
    "authors": [
        {
            "name": "wataru",
            "email": "wataru775@gmail.com"
        }
    ],
    "require": {}
}

Do you confirm generation [yes]? 
Would you like to install dependencies now [yes]? 
Loading composer repositories with package information
Updating dependencies
Lock file operations: 34 installs, 0 updates, 0 removals
  - Locking doctrine/instantiator (1.4.0)
  - Locking myclabs/deep-copy (1.10.2)
  - Locking nikic/php-parser (v4.10.5)
  - Locking phar-io/manifest (2.0.1)
  - Locking phar-io/version (3.1.0)
  - Locking phpdocumentor/reflection-common (2.2.0)
  - Locking phpdocumentor/reflection-docblock (5.2.2)
  - Locking phpdocumentor/type-resolver (1.4.0)
  - Locking phpspec/prophecy (1.13.0)
  - Locking phpunit/php-code-coverage (9.2.6)
  - Locking phpunit/php-file-iterator (3.0.5)
  - Locking phpunit/php-invoker (3.1.1)
  - Locking phpunit/php-text-template (2.0.4)
  - Locking phpunit/php-timer (5.0.3)
  - Locking phpunit/phpunit (9.5.4)
  - Locking sebastian/cli-parser (1.0.1)
  - Locking sebastian/code-unit (1.0.8)
  - Locking sebastian/code-unit-reverse-lookup (2.0.3)
  - Locking sebastian/comparator (4.0.6)
  - Locking sebastian/complexity (2.0.2)
  - Locking sebastian/diff (4.0.4)
  - Locking sebastian/environment (5.1.3)
  - Locking sebastian/exporter (4.0.3)
  - Locking sebastian/global-state (5.0.2)
  - Locking sebastian/lines-of-code (1.0.3)
  - Locking sebastian/object-enumerator (4.0.4)
  - Locking sebastian/object-reflector (2.0.4)
  - Locking sebastian/recursion-context (4.0.4)
  - Locking sebastian/resource-operations (3.0.3)
  - Locking sebastian/type (2.3.1)
  - Locking sebastian/version (3.0.2)
  - Locking symfony/polyfill-ctype (v1.22.1)
  - Locking theseer/tokenizer (1.2.0)
  - Locking webmozart/assert (1.10.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 34 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-ctype (v1.22.1): Extracting archive
  - Installing webmozart/assert (1.10.0): Extracting archive
  - Installing phpdocumentor/reflection-common (2.2.0): Extracting archive
  - Installing phpdocumentor/type-resolver (1.4.0): Extracting archive
  - Installing phpdocumentor/reflection-docblock (5.2.2): Extracting archive
  - Installing sebastian/version (3.0.2): Extracting archive
  - Installing sebastian/type (2.3.1): Extracting archive
  - Installing sebastian/resource-operations (3.0.3): Extracting archive
  - Installing sebastian/recursion-context (4.0.4): Extracting archive
  - Installing sebastian/object-reflector (2.0.4): Extracting archive
  - Installing sebastian/object-enumerator (4.0.4): Extracting archive
  - Installing sebastian/global-state (5.0.2): Extracting archive
  - Installing sebastian/exporter (4.0.3): Extracting archive
  - Installing sebastian/environment (5.1.3): Extracting archive
  - Installing sebastian/diff (4.0.4): Extracting archive
  - Installing sebastian/comparator (4.0.6): Extracting archive
  - Installing sebastian/code-unit (1.0.8): Extracting archive
  - Installing sebastian/cli-parser (1.0.1): Extracting archive
  - Installing phpunit/php-timer (5.0.3): Extracting archive
  - Installing phpunit/php-text-template (2.0.4): Extracting archive
  - Installing phpunit/php-invoker (3.1.1): Extracting archive
  - Installing phpunit/php-file-iterator (3.0.5): Extracting archive
  - Installing theseer/tokenizer (1.2.0): Extracting archive
  - Installing nikic/php-parser (v4.10.5): Extracting archive
  - Installing sebastian/lines-of-code (1.0.3): Extracting archive
  - Installing sebastian/complexity (2.0.2): Extracting archive
  - Installing sebastian/code-unit-reverse-lookup (2.0.3): Extracting archive
  - Installing phpunit/php-code-coverage (9.2.6): Extracting archive
  - Installing doctrine/instantiator (1.4.0): Extracting archive
  - Installing phpspec/prophecy (1.13.0): Extracting archive
  - Installing phar-io/version (3.1.0): Extracting archive
  - Installing phar-io/manifest (2.0.1): Extracting archive
  - Installing myclabs/deep-copy (1.10.2): Extracting archive
  - Installing phpunit/phpunit (9.5.4): Extracting archive
5 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
26 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

これを実行で生成されるファイルは…

composer.json
composer.lock
vendor

次に、おまじない(理解してね)を指定します。
composer.jsonにソースとテストのクラスパスを指定します

...
    "autoload": {
        "classmap": [
            "src/"
        ],
        "psr-4": {
            "org\\mmpp\\learning\\phpunit\\": "src"
        }
    },
    "autoload-dev": {
        "classmap": [
            "tests/"
        ],
        "psr-4": {
            "Tests\\Unit\\org\\mmpp\\learning\\phpunit\\": "tests/Unit",
            "Tests\\Feature\\org\\mmpp\\learning\\phpunit\\": "tests/Feature"
        }
    }
...

autoloaderに追記しましたので反映!!

$ composer dumpautoload
Generating autoload files
Generated autoload files

これのために、大分時間がかかりましたw

どきどき、テストを書く…

簡単なテストを書きましょう!アイデアを考えるのが面倒くさいので、マニュアルから拝借

https://phpunit.readthedocs.io/ja/latest/writing-tests-for-phpunit.html

Example 2.1を実装!

https://github.com/wataru775/learning-phpunit/blob/PHPUnit%E3%83%9E%E3%83%8B%E3%83%A5%E3%82%A2%E3%83%AB/tests/Unit/PHPUnitOfficial/Example2_1Test.php

<?php


namespace Tests\Unit\org\mmpp\learning\phpunit\PHPUnitOfficial;

use PHPUnit\Framework\TestCase;

/**
 * PHPUnitマニュアル Example 2.1 PHPUnit での配列操作のテスト
 * Class Example2_1Test
 * @package Tests\Unit\org\mmpp\learning\phpunit\PHPUnitOfficial
 * @see https://phpunit.readthedocs.io/ja/latest/writing-tests-for-phpunit.html#writing-tests-for-phpunit-examples-stacktest-php
 */
class Example2_1Test extends TestCase
{
    public function testPushAndPop()
    {
        $stack = [];
        $this->assertSame(0, count($stack));

        array_push($stack, 'foo');
        $this->assertSame('foo', $stack[count($stack)-1]);
        $this->assertSame(1, count($stack));

        $this->assertSame('foo', array_pop($stack));
        $this->assertSame(0, count($stack));
    }

}

これで、動作確認をすると?

$ ./vendor/phpunit/phpunit/phpunit .
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.002, Memory: 4.00 MB

OK (1 test, 5 assertions)

はい、通りました!ぱふぱふ!

クラスをテストする

良きサンプルがなかったので、作りました。
今回のテストはTDDで進めました。
TDDの場合は対象のクラスがないのでちょっと引っかかるかもしれませんが以下のようにはじめにテストを書きます

<?php
namespace Tests\Unit\org\mmpp\learning\phpunit\Original;

use org\mmpp\learning\phpunit\Original\HelloWorld;
use PHPUnit\Framework\TestCase;

/**
 * [オリジナル] ハロー・ワールドのクラスのテスト
 * Class HelloWorldTest
 * @package Tests\Unit\org\mmpp\learning\phpunit\Original
 */
class HelloWorldTest extends TestCase
{
    /**
     * ハローを返すだけのテスト
     */
    public function test_say(){
        $hello_world = new HelloWorld();
        $this->assertEquals('hello',$hello_world->say());
    }
}

難しくはないです、HelloWorldのクラスを作成して、メソッドsayを実行すればhelloを返すだけです…
実装すると src以下にHelloWorldを作ります

<?php
namespace org\mmpp\learning\phpunit\Original;

/**
 * [オリジナル] ハロー・ワールドのクラス
 * Class HelloWorld
 * @package org\mmpp\learning\phpunit\Original
 */
class HelloWorld
{
    /**
     * @return string hello
     */
    public function say(){
        return 'hello';
    }
}

これで、実行するが…

$ ./vendor/phpunit/phpunit/phpunit .
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

E.                                                                  2 / 2 (100%)

Time: 00:00.004, Memory: 6.00 MB

There was 1 error:

1) Tests\Unit\org\mmpp\learning\phpunit\Original\HelloWorldTest::test_say
Error: Class 'org\mmpp\learning\phpunit\Original\HelloWorld' not found

learning_phpunit/tests/Unit/Original/HelloWorldTest.php:20

ERRORS!
Tests: 2, Assertions: 5, Errors: 1.

HelloWorld’ not foundだよぉ〜;;

意味不明なおまじない(そろそろ理解できるでしょw)

$ composer dumpautoload
Generating autoload files
Generated autoload files

と、おまじない…

$ ./vendor/phpunit/phpunit/phpunit .
PHPUnit 9.5.4 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 00:00.003, Memory: 6.00 MB

OK (2 tests, 6 assertions)

てっててぇ〜〜〜

これで無事に出来ました!
おまじないなしの方法を考えねばな

dumpautoloadならぬautoloaddumpautoload … おまじないかよ!

https://github.com/wataru775/learning-phpunit/blob/PHPUnit%E3%83%9E%E3%83%8B%E3%83%A5%E3%82%A2%E3%83%AB/src/Original/HelloWorld.php

https://github.com/wataru775/learning-phpunit/blob/PHPUnit%E3%83%9E%E3%83%8B%E3%83%A5%E3%82%A2%E3%83%AB/tests/Unit/Original/HelloWorldTest.php