Laravel、CakePHPを使わないテストコードの実装方法

# はじめに
プロダクトの品質を判断する基準にもなるテストコード。
ここではLaravelやCakePHPを使わないケースで、テストコードを実装する方法を紹介します。

UIを持たないスクリプトにもテストコードを実装して、より皆が安心できる開発を実践していこう☺️

# この記事の対象者
- PHPを学習中の方
- フレームワークを使用せずにテストコードを実装したい方
- DockerのPHP環境を構築したい方(フレームワークはなし)

# 手順
1. DockerでのPHP環境構築
2. ComposerでのPHPUnitを実装
3. 名前空間(namespace)の指定
4. テストコードの実装
5. テストの実施

# DockerでのPHP環境構築
Dockerfileを使用して、PHPの動作環境を構築します。

### Dockerfileの記述

```dockerfile
FROM amazonlinux:2
RUN yum -y update && \
    yum clean all && \
    yum install -y sudo && \
    yum install -y vim && \
    yum install -y systemd && \
    yum install -y yum-utils && \
    yum install -y unzip && \
    yum install -y wget

# xxxxxのところはログインするユーザー名に変えてください
# 2行とも変更が必要です。
RUN useradd xxxxx && \
    echo "xxxxx ALL=NOPASSWD: ALL" >> /etc/sudoers

# ここでPHPをインストールしています
RUN amazon-linux-extras install php7.4 && \
    yum -y update && \
    yum install -y php-dom && \
    yum install -y php-zip && \
    yum install -y php-mbstring && \
    yum install -y php-xml

RUN yum install -y glibc-langpack-ja && yum clean all
ENV LANG ja_JP.utf8
ENV LC_ALL ja_JP.utf8
RUN unlink /etc/localtime
RUN ln -s /usr/share/zoneinfo/Japan /etc/localtime

# ここから下でComposerをダウンロードからインストールまでやります
# 単体テストツールのPHPUnitをComposerから適用するので必要になります
RUN curl -sS https://getcomposer.org/installer | php
RUN mv composer.phar /usr/local/bin/composer
RUN curl --silent --location https://rpm.nodesource.com/setup_10.x | sudo bash -
RUN yum install -y git
ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/composer/vendor/bin

# dockerコンテナにログインした時のカレントディレクトリを指定しています
WORKDIR /usr/local/sbin
```

### Dockerイメージのビルド


```Bash:Terminal
# 次のコマンドでは、自分が作業しているディレクトリにあるDockerfileを使用します。
# Dockerfileを別の場所に置いている方は . をDockerfileのあるディレクトリのパスに変更してください
# よくわからない場合は、作業しているディレクトリにDockerfileを移動させてください
bearrabi@bearrabi-macmini PHPUnit % docker build . -t phpunit_img
.
.
.
.

# ここでSuccessfullyと表示されたらビルドできています
Successfully built a603b3a4ede7
Successfully tagged phpunit_img:latest
```


### Dockerコンテナ内で作業する時のディレクトリを作成
```Bash:Terminal
bearrabi@bearrabi-macmini PHPUnit % cd work
```

### ビルドしたDockerイメージからコンテナを生成
```Bash:Terminal
# -d: 裏側で実行するよ　※これがないと、次に進めなくなるよ
# --privileged: 管理者で実行する権限を与えるよ　※これがないと、コンテナ内でいくつかのコマンドが使用できないよ
# --name: コンテナに名前をつけるよ　※これがないと、コンテナが増えた時に判別が難しくなるよ
# -v: ローカルのディレクトリとコンテナ内のディレクトリを紐付けするよ　※これがないとエディタで変更した内容がコンテナ内に反映されないよ
bearrabi@bearrabi-macmini PHPUnit % docker run -d --privileged --name phpunit_env -v /Users/bearrabi/work/qiita/PHPUnit/work:/usr/local/sbin:rw phpunit_img /sbin/init

# こんな文字列が表示されたらコンテナが生成されています。
# 毎回違うので、全く同じ文字列でなくても問題ありません。
6b7d1624333e76852519e440b501e70573d51f1d2371d2755433ed15f38af621

# 不安であれば、次のコマンドを実行して、docker run コマンドの --nameで指定した名前があるか確認しましょう。
# 今回のケースでは、「phpunit_env」という名前のコンテナが生成していればOKです
bearrabi@bearrabi-macmini PHPUnit % docker ps -a

# 一番右のNAMESの下にphpunit_envが表示されていると思います
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                    PORTS               NAMES
6b7d1624333e        phpunit_img         "/sbin/init"        6 seconds ago       Up 6 seconds                                  phpunit_env
```

### Dockerコンテナにログイン
```Bash:Terminal
# xxxxxのところはDockerfileで記述したユーザーの名前を入力してください。
bearrabi@bearrabi-macmini PHPUnit % docker exec -it -u xxxxx phpunit_env /bin/bash

# こんな表示に変わったらログインできてます
# ]の前がsbinになっていることだけは確認してください。　※なっていないと、docker runコマンドの-vオプションが効いてません
[xxxxx@6b7d1624333e sbin]$ 
```

# ComposerでのPHPUnitを実装
ここからPHPUnitを使える状態にしていきます。

### PHPUnitの実装
```Bash:Terminal
[xxxxx@6b7d1624333e sbin]$ composer require "PHPUnit/PHPUnit"
.
.
.
# 長いです
.
.
.

# エラーが出ていなければOKです
# 念のため、関連するファイルとディレクトリが生成されていることを確認しましょう
# 次のようになればOKです
[xxxxx@6b7d1624333e sbin]$ ls
composer.json  composer.lock  vendor
```
# 名前空間(namespace)の指定
名前空間を利用することで、複数のファイルに同じクラス名が存在しても識別することができます。

### composer.jsonに記述を追加
comopser.jsonはvendorディレクトリと同じ階層に作成されます。
"autoload"の行からが追加した内容です。
ここでは名前空間「App」がappディレクトリと紐づくように名前空間を指定してますね。
##### !!!カンマとか、{}の抜けに注意!!!

```json:composer.json
{
    "require": {
        "phpunit/phpunit": "^9.4"
    },
    "autoload": {
        "psr-4": {
            "App\\" : "app/"
        }
    }

}
```

### composer.jsonの再読み込み
```Bash:Terminal
# これを実行しないと、comopser.jsonに記述した内容が適用されません。
[xxxxx@6b7d1624333e sbin]$ composer dump-autoload
Generating autoload files
Generated autoload files
```


# テストコードの実装
今回は2つのファイルに存在するaddメソッドをテストすることにします。

### ディレクトリの作成
```Bash:Terminal
# appディレクトリの作成　※今回はappディレクトリ以下をテストする対象とします
[xxxxx@6b7d1624333e sbin]$ mkdir app

# appディレクトリ配下にArticleディレクトリとCaluculationディレクトリを作成
[xxxxx@6b7d1624333e sbin]$ mkdir app/Article
[xxxxx@6b7d1624333e sbin]$ mkdir app/Calculation

# Testsディレクトリの作成　※テストコードをTestsディレクトリ以下に配置するとします
[xxxxx@6b7d1624333e sbin]$ mkdir Tests
```

### テスト対象のコードの実装

##### Article
記事に文章を追加することを想定して、文字列の結合を行うfunctionを実装しました。

```php:app/Article/Normal.php
<?php

// ここで、このファイルの名前空間を指定
namespace App\Article;

class Normal{
  public static function add(String $source, String $append)
  {
    // 引数で渡された文字列を結合して返します。
    return $source.$append;
  }
}
```

##### Calculation
数値を加算することを想定して、足し算を行うfunctionを実装しました。

```php:app/Calculation/Normal.php
<?php

// ここでこのファイルの名前空間を指定しています
namespace App\Calculation;

class Normal{
  public static function add(int $added_num, int $add_num)
  {
    //足し算結果を返します
    return $added_num + $add_num;
  }
}
```

### テストコードの記述
```php:Tests/Article/TestNormal.php
<?php
use PHPUnit\Framework\TestCase;

// ここでapp/Articleとapp/Calculationの名前空間を呼び出しています
use App\Article;
use App\Calculation;

class TestNormal extends TestCase{

  public function testAdd(){

    // ここでArticleディレクトリのNormalクラスからadd functionを呼び出し
    // composer.jsonで名前空間を指定することで、特定のディレクトリのクラスとfunctionを指定している
    $article_add_result = Article\Normal::add("1", "2");

    // 文字が結合できればテストをパスするコード
    $this->assertEquals("12", $article_add_result);

    //ここでCalculationディレクトリのNormalクラスからadd functionを呼び出し
    // composer.jsonで名前空間を指定することで、特定のディレクトリのクラスとfunctionを指定している
    $calc_add_result = Calculation\Normal::add(1, 2);

    // 足し算ができればテストをパスするコード
    $this->assertEquals(3, $calc_add_result);

  }
}
```


# テストの実施
実際にテストコードを実行して、テストをしてみましょう

### TestNormal.phpをPHPUnitで実行
```Bash:Terminal
# phpコマンドでphpunitを実行します。
# --colorを忘れると、全部白文字になるので、テスト結果がわかりにくくなります
[xxxxx@6b7d1624333e sbin]$ php vendor/phpunit/phpunit/phpunit Tests/TestNormal.php --color
PHPUnit 9.4.3 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.062, Memory: 4.00 MB

OK (1 test, 2 assertions)
```

### コマンドの簡略化
何度も実行するにはコマンドが長いですね。
なので、aliasを使って簡略化しておきましょう

```Bash:Terminal
# punit と入力すれば 「php vendor/phpunit/phpunit/phpunit --color」までを実行してくれるようにする
[xxxxx@6b7d1624333e sbin]$ alias punit='php vendor/phpunit/phpunit/phpunit --color'

# 簡略化したコマンドでもう一度テストを実施してみよう
[xxxxx@6b7d1624333e sbin]$ punit Tests/TestNormal.php 
PHPUnit 9.4.3 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.064, Memory: 4.00 MB

OK (1 test, 2 assertions)
```

問題なくテストが完了しましたね。


# 注意点
この記事では読みやすさを優先したので、テストコードを後に記述するようにしました。
しかし、より正しい手順はテストコードを先に書いて、テスト対象のコードを後に書くことです。
納期の関係でテストコードを実装しない場合もありますが、バグが発生しやすい箇所にはチームで相談してテストコードを実装しましょう。

# 最後に
今回はテストコードについて記載してきました。

テストコード実装の目的はテストの自動化にフォーカスされがちです。しかし、__*テストしやすいコードを意識することで、プログダクトの複雑化を防止することにも繋がります。*__
１つのメソッドに何個も処理があるとテストコードを書きにくくて仕方ないですからね。

バグがないことをテストするのも大事ですが、そもそもバグが発生しにくい開発を心がけたいものです。

それでは、最後まで読んでいただき、ありがとうございます☺️