【PHP版】CI(継続的インテグレーション)ツール導入ガイド:第3回 さまざまなジョブ(1)

前の記事では、第2回 さまざまなタスク 「CIの実行中に処理されるジョブ」についての概要を紹介した。CIツール導入ガイドの第3回はさまざまなジョブ(1)を紹介する。

CIのジョブとして利用可能なPHP用のツールとして、以下のような物が有名だ。

名称 役割
PHPMD 不要変数分析、循環的複雑度解析
PHPCPD 重複コード検出
PHP_CodeSniffer コーディングスタイルチェック、および修正
PHP-CS-Fixer コーディングスタイルチェック、および修正
phpDocumentor ドキュメント自動生成
phpdox ドキュメント自動生成
PHPUnit ユニットテスト
xdebug カバレッジ解析
php7cc PHP7との互換性チェック
Selenium UIテスト
Phing バッチ処理
Jenkins 統合ツール
GitLab 統合ツール

今回これらのうちから PHPMD, PHPCPD そして PHPUnit を紹介したい。個々のツールの紹介は駆け足になってしまうが、具体的にどのようにインストールし、どういう動作をするのかについての大まかな雰囲気を掴んでもらいたい。

インストール環境

各ツールは、通常コマンドラインからの手動操作で実行可能であるが、前述した通り CIツールから自動的に実行するのが望ましい。自動実行の設定については第5回で紹介しよう。今回はそれぞれのツールを個別にインストールし、実行した結果を紹介していきたい。

最初に今回インストール手順を紹介する全ツールで共通の初期設定を紹介しよう。本稿における実行環境としては CentOS 7 を利用する事とした。まずは minimal インストールした CentOS7で以下を実行し、php をインストールして頂きたい

sodu yum install -y php php-xml

さらにPHPのライブラリ管理を行う composer を導入したい。composer は最近の PHPプロジェクトでよく使われるライブラリ管理ツールであり、従来の PEAR に置き換わる物である。

curl -LsS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin
echo 'export PATH=$PATH:$HOME/.composer/vendor/bin' >> ~/.bashrc
bash ~/.bashrc

これで各ツールをインストールする準備が整った。個々のツールのインストールについては順次説明していきたい。

サンプルプログラム

以下は各ツールの動作を検証するためのサンプルプログラムだ。以下2つのファイルを

 ホームディレクトリ/class 

に保存して欲しい。

class/SampleClass1.php

<?php
class Sample_Class1
{
  public function sample1($a, $b, $c, $d, $e, $f, $g, $h)
  {
     $check_num = 1;
     $out = 0;
     $tmp1 = 10000;
     $tmp2 = 10000;
     $tmp3 = 10000;
     $tmp4 = 10000;
     $tmp5 = 10000;
     $tmp6 = 10000;
     $tmp7 = 10000;
     $tmp8 = 10000;
     $tmp9 = 10000;
     if ($a == $check_num) {
        $out++;
     }
     if ($b == $check_num) {
        $out++;
     }
     if ($c == $check_num) {
        $out++;
     }
     if ($d == $check_num) {
        $out++;
     }
     if ($e == $check_num) {
        $out++;
     }
     if ($f == $check_num) {
        $out++;
     }
     if ($g == $check_num) {
        $out++;
     }
     if ($h == $check_num) {
        $out++;
     }
     $out = 1000 + $out;
     return $out;
  }
}

SampleClass2.php

<?php
class Sample_Class2
{
  public function sample1($a, $b, $c, $d, $e, $f, $g, $h)
  {
     $check_num = 2;
     $out = 0;
     $tmp1 = 10000;
     $tmp2 = 10000;
     $tmp3 = 10000;
     $tmp4 = 10000;
     $tmp5 = 10000;
     $tmp6 = 10000;
     $tmp7 = 10000;
     $tmp8 = 10000;
     $tmp9 = 10000;
     if ($a == $check_num) {
        $out++;
     }
     if ($b == $check_num) {
        $out++;
     }
     if ($c == $check_num) {
        $out++;
     }
     if ($d == $check_num) {
        $out++;
     }
     if ($e == $check_num) {
        $out++;
     }
     if ($f == $check_num) {
        $out++;
     }
     if ($g == $check_num) {
        $out++;
     }
     if ($h == $check_num) {
        $out++;
     }
     $out = 1000 + $out;
     return $out;
  }
}

Sample_Class1 は与えられた8個の変数のうち、値が1だった引数の数に1000を足した物を、Sample_Class2 は値が2だった引数の数に1000を足した物を求めるためのクラスだ。
見ての通り色々と問題だらけのコードだが、これらが各ツールによってどのように評価されるかを確認していきたい。

PHPMD

公式サイト::
https://phpmd.org/

静的コード解析を行うツールである。不要変数、循環的複雑度解析等を行ってくれる。インストールは以下の通りだ。

composer.phar global require phpmd/phpmd

コマンドラインの書式は以下の通りである。

~/.composer/vendor/bin/phpmd [オプション] <ファイル名> <出力形式> <ルールセット>

<ファイル名>には対処とするファイルを指定する。ここにディレクトリ名を指定すれば、サブディレクトリも含め配下のファイルをすべて検査してくれる。<出力形式> は “xml”、または “text” のいずれかを指定する。

最後の<ルールセット>は、PHPMDで評価したいルールの一覧をカンマ区切りで指定する。以下の6種類のいずれか、またはすべてをカンマ区切りで指定可能だ。

cleancode クリーンなコードかどうかを評価するルール
codesize プログラムのコードや大きさに関するルール
controversial 評価が一定ではないルール
design コードの書き方に関するルール
naming 変数名等に関するルール
unusedcode

未使用変数等に関するルール

なかなか分かりにくい物もあるが、実際に実行してみる事で何を言われているか理解できる事も多い。以下は実行例だ。

# ~/.composer/vendor/bin/phpmd class/SampleClass1.php text cleancode,codesize,controversial,design,naming,unusedcode
/root/class/SampleClass1.php:2  The class Sample_Class1 is not named in CamelCase.
/root/class/SampleClass1.php:4  The method sample1() has an NPath complexity of 256. The configured NPath complexity threshold is 200.
/root/class/SampleClass1.php:4  The variable $check_num is not named in camelCase.
/root/class/SampleClass1.php:4  The variable $check_num is not named in camelCase.
/root/class/SampleClass1.php:4  The variable $check_num is not named in camelCase.
/root/class/SampleClass1.php:4  The variable $check_num is not named in camelCase.
/root/class/SampleClass1.php:4  The variable $check_num is not named in camelCase.
/root/class/SampleClass1.php:4  The variable $check_num is not named in camelCase.
/root/class/SampleClass1.php:4  The variable $check_num is not named in camelCase.
/root/class/SampleClass1.php:4  The variable $check_num is not named in camelCase.
/root/class/SampleClass1.php:4  The variable $check_num is not named in camelCase.
/root/class/SampleClass1.php:4  Avoid variables with short names like $a. Configured minimum length is 3.
/root/class/SampleClass1.php:4  Avoid variables with short names like $b. Configured minimum length is 3.
/root/class/SampleClass1.php:4  Avoid variables with short names like $c. Configured minimum length is 3.
/root/class/SampleClass1.php:4  Avoid variables with short names like $d. Configured minimum length is 3.
/root/class/SampleClass1.php:4  Avoid variables with short names like $e. Configured minimum length is 3.
/root/class/SampleClass1.php:4  Avoid variables with short names like $f. Configured minimum length is 3.
/root/class/SampleClass1.php:4  Avoid variables with short names like $g. Configured minimum length is 3.
/root/class/SampleClass1.php:4  Avoid variables with short names like $h. Configured minimum length is 3.
/root/class/SampleClass1.php:8  Avoid unused local variables such as '$tmp1'.
/root/class/SampleClass1.php:9  Avoid unused local variables such as '$tmp2'.
/root/class/SampleClass1.php:10 Avoid unused local variables such as '$tmp3'.
/root/class/SampleClass1.php:11 Avoid unused local variables such as '$tmp4'.
/root/class/SampleClass1.php:12 Avoid unused local variables such as '$tmp5'.
/root/class/SampleClass1.php:13 Avoid unused local variables such as '$tmp6'.
/root/class/SampleClass1.php:14 Avoid unused local variables such as '$tmp7'.
/root/class/SampleClass1.php:15 Avoid unused local variables such as '$tmp8'.
/root/class/SampleClass1.php:16 Avoid unused local variables such as '$tmp9'.

上記は SampleClass1.php に適用した例だが、警告内容を1行ずつ見てみよう。

/root/class/SampleClass1.php:2  The class Sample_Class1 is not named in CamelCase.

クラス名が CamelCase ではない事が警告されている。クラス名を SampleClass1 とする事でこの警告を抑止できる。

/root/class/SampleClass1.php:4  The method sample1() has an NPath complexity of 256. The configured NPath complexity threshold is 200.

これは sample1() 関数の NPath が256通りあるという警告だ。 NPath とはプログラムの通り道である。この関数では$a ~ $h の8個の引数それぞれにつき、値が1であるかどうかで場合分けを行っている。それが8ヵ所あるのだから、引数の値の組み合わせにより、プログラムの経路は2の8乗で256通りとなる。つまり sample1() 関数のすべての経路をテストするためには256回のテストが必要である事を警告している。当然ながら NPath はなるべく少ない方が良い。

/root/class/SampleClass1.php:4  Avoid variables with short names like $a. Configured minimum length is 3.

クラス名に対する警告と同じく、変数名が camelCase ではない事が警告されている。

/root/class/SampleClass1.php:4  Avoid variables with short names like $a. Configured minimum length is 3.

変数名が短すぎる事が警告されている。

/root/class/SampleClass1.php:8  Avoid unused local variables such as '$tmp1'.

$tmp1 が利用されていない事を警告されている。

このようにさまざまな警告が表示された。ところでクラス名や変数名に CamelCase しか受け付けない事はルールが厳しすぎる、もしくは妥当ではないと感じるかも知れない。また1文字の変数名にしても、状況によっては警告されたくない場合も多い。これらのコーディング規約に属するようなルールは、naming ルールセットや controversial ルールセットに含まれている。これらのルールセットを外す事で、より構造的な問題だけが指摘されるようになる。以下は実行例だ。

# ~/.composer/vendor/bin/phpmd class/SampleClass1.php text cleancode,codesize,design,unusedcode
/root/class/SampleClass1.php:4  The method sample1() has an NPath complexity of 256. The configured NPath complexity threshold is 200.
/root/class/SampleClass1.php:8  Avoid unused local variables such as '$tmp1'.
/root/class/SampleClass1.php:9  Avoid unused local variables such as '$tmp2'.
/root/class/SampleClass1.php:10 Avoid unused local variables such as '$tmp3'.
/root/class/SampleClass1.php:11 Avoid unused local variables such as '$tmp4'.
/root/class/SampleClass1.php:12 Avoid unused local variables such as '$tmp5'.
/root/class/SampleClass1.php:13 Avoid unused local variables such as '$tmp6'.
/root/class/SampleClass1.php:14 Avoid unused local variables such as '$tmp7'.
/root/class/SampleClass1.php:15 Avoid unused local variables such as '$tmp8'.
/root/class/SampleClass1.php:16 Avoid unused local variables such as '$tmp9'.

以上のように、PHPMD ではルールセットを選びながら必要な警告だけを取り出す事ができる。また本稿では触れないが独自にルールセットを作成する事も可能だ。まずはルールセットとして

cleancode,codesize,design,unusedcode

を利用し、必要に応じてカスタマイズしていくのが良いだろう。

PHPMD によって指摘される不要変数分析は、スクリプト言語にとって特に有効で、すぐ役に立つ。不要な変数が存在しているという事は、意図せず似た名前の変数が存在している可能性があり、バグの原因になる。見つけたら適切な処理をするようにしよう。
一方、循環的複雑度解析の結果は警告されても困る場合も多いかも知れない。すでに複雑なコードをリファクタリングするのはなかなか手間もかかる作業となり、スケジュールとの兼ね合いを考えたら処理しにくい場合も多いだろう。他のツールによる指摘の場合もそうだが、問題点を認識しつつも優先度の高い処理を先に処理するようにしよう。

PHPCPD

公式サイト::
https://github.com/sebastianbergmann/phpcpd

PHPCPD も静的コード解析を行うツールだ。単一、または複数のソースファイルの中から重複した部分を指摘してくれる PHPCPD を利用する事で、コピー&ペーストで構築されてしまったコードを見つける事ができる。インストールは以下の通りである。

composer.phar global require sebastian/phpcpd

コマンドラインの書式は以下の通りである。

~/.composer/vendor/bin/phpcpd [オプション] <ディレクトリ名>

実行すると<デイレクトリ名>配下にあるソースファイルを調査し、重複箇所を指摘してくれる。オプションにはさまざまな物があるが、特に必要なのは “–name” オプションである。これは対象とするファイルのファイル名のパターンを指定する物であり、拡張子が php 以外のファイルも対象とする場合に “–name=’*.php;*.inc’ 等のように利用する。

実行例は以下の通りだ。

# ~/.composer/vendor/bin/phpcpd class
phpcpd 2.0.4 by Sebastian Bergmann.

Found 1 exact clones with 36 duplicated lines in 2 files:

  -     /root/class/SampleClass1.php:7-43
        /root/class/SampleClass2.php:7-43

40.00% duplicated lines out of 90 total lines of code.

Time: 18 ms, Memory: 2.50MB

この例では SampleClass1.php と SampleClass2.php が酷似している事を警告している。このように簡単に重複箇所を確認可能だ。

似て非なるコードの存在を許す事は、メンテナンス性の悪さを許容する事になる。コピペで増殖した似て非なるコードの1ヵ所にバグが見つかれば、その他の箇所にもバグはあると考えるべきだろう。当然修正していかなければならないのだが、完全に一致しているわけではないために機械的な置換では却って問題を大きくしてしまったりする場合もあり、慎重な対応が必要だ。このような問題を回避していくために、 PHPCPD による指摘は積極的に解決していきたい。ただし、PHPMD の時にも述べたように、重要な問題が他にあればそちらを優先すべきである。

なお、PHPMD ではあまり短い類似点は警告されないので注意して欲しい。短い類似でもメンテナンス性を悪くするコードは色々ある。代表的なのがマジックナンバーだ。たとえば消費税率の値をプログラム中の至る所に 0.08 と記述しておけば、消費税率が更新された時にすべて修正しなければならない。うっかり一括置換した時に消費税率以外の何かを示す 0.08 まで置換してしまうと、バグを誘発するだろう。ツールに頼り切らず、こんな点も注意しながらプログラミングしていきたい所だ。

PHPUnit

PHPUnit はユニットテストを行うための CIツールである。ユニットテストをCIに導入できればデグレードを素早く発見できる。インストール方法は以下の通りである。

yum install epel-release
yum install php-pecl-xdebug
composer.phar global require phpunit/phpunit

コマンドラインの書式は以下の通りである。

~/.composer/vendor/bin/phpunit [オプション] <ディレクトリ名>

<ディレクトリ名>で指定したディレクトリ以下に配置されているphpプログラムのユニットテストを行う。オプションにはさまざまな物があるが、取り合えずは何も指定しなくても良い。

ユニットテストを行いたい場合、テストスクリプトが必要になる。これはテストしたいクラスや関数を実際に呼び出し、適切な動作を行っているかの確認をするためのプログラムだ。PHPUnit のようなツールを前提としないテストスクリプトを書く事ももちろん可能だが、ツールを利用する事で効率的に記述できる。今回の場合、SampleClass1.php をテストするコードは以下のような感じになる。保存先ディレクトリは test としよう。

test/SampleClass1Test.php

<?php
require_once __dir__ . '/../class/SampleClass1.php';

class Sample_Class1Test extends PHPUnit\Framework\TestCase
{
    protected $object;

    protected function setUp()
    {
        $this->object = new Sample_Class1();
    }

    public function test_sample1_1()
    {
        $this->assertEquals($this->object->sample1(1,1,1,1,1,1,1,1), 1008);
    }

    public function test_sample1_2()
    {
        $this->assertEquals($this->object->sample1(2,2,2,2,2,2,2,2), 1000);
    }

    public function test_sample1_3()
    {
        $this->assertEquals($this->object->sample1(1,2,1,2,1,2,1,2), 1004);
    }
}

テスト用クラスは PHPUnit\Framework\TestCase から継承して作成する。なお、このクラスは PHPUnit のブートストラップコードによって認識可能になっているので、敢えて require する必要は無い。ただし、テスト対象となるクラスについては require する必要がある。

setUp() メソッドはテスト実行前に必ず実行される内容を記載する。ここではテスト対象となるクラスオブジェクトを構築している。それぞれのテストは、名前が “test”で始まるメソッドとして定義している。テスト対象のメソッドかどうかは名前で認識しているので、必ず “test” というプレフィックスを付けたメソッドとして定義しなくてはならない。逆に言えば “test” というプレフィックスが付かないメソッドは通常のメソッドとして利用可能だ。

各テストメソッドでは、assertEquals() メソッド等を利用して、各テストを記述する。このメソッドは第1引数と第2引数が等しい時にテストに成功したと判断するメソッドだ。ここでは Sample_Class1Test クラスの sample1() メソッドの引数にさまざまな値を代入した結果と、その戻り値として期待する結果を比較している。

このテストを実行した結果は以下の通りだ。

# ~/.composer/vendor/bin/phpunit test
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

...

Time: 47 ms, Memory: 4.00MB

OK (3 tests, 3 assertions)

ピリオドが3つ出力されているが、これは3つのテストを実行してすべて成功した事を意味している。ここで test_sample1_2() を以下の内容に書き得よう。

    public function test_sample1_2()
    {
        $this->assertEquals($this->object->sample1(2,2,2,2,2,2,2,2), 0);
    }

実行結果は以下のようになる。

# ~/.composer/vendor/bin/phpunit test
PHPUnit 4.8.36 by Sebastian Bergmann and contributors.

.F.

Time: 48 ms, Memory: 4.25MB

There was 1 failure:

1) Sample_Class1Test::test_sample1_2
Failed asserting that 0 matches expected 1000.

/root/test/SampleClass1Test.php:20

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

先ほどは

 ... 

と出力されていた部分が

 .F. 

に置き換わり、2番目のテストが失敗した事が分かる。また

1) Sample_Class1Test::test_sample1_2
Failed asserting that 0 matches expected 1000.

と、出力されている通り、test_sample1_2 が失敗しており、期待する結果が0であるのに対し、実際にメソッドを実行した結果は 1000である事が分かるようになっている。

ところで sample1() メソッドは8個の引数を取り、各引数が1である数の合計に1000を足した値を返すメソッドだ。このクラスを精度良くテストしたいと思ったら、各引数が1かそうでないかの組み合わせをもう少しテストした方が良いだろう。理想としてはそれぞれの引数が1の場合とそうでない場合で、合計256通りのテストをしたい所だ。これは非常に面倒な作業となるのだが、sample1() メソッドの設計に問題があるからそうなってしまったと言える。これが PHPMD で指摘された、

The method sample1() has an NPath complexity of 256. 

という事に他ならない。

PHPUnit には他にもさまざまな機能があるが、基本的な利用方法は今説明した通りだ。テストしたいメソッドを実行し、その結果を期待する結果と比較する事でテストが成功したかどうかを判定している。PHPMD や PHPCPD と違い、実際にコードの一部を実行して評価している点が特徴だ。テストスクリプトを用意するのは面倒だが、用意できればバージョンアップの際等に、これまでのプログラムが問題なく動く事を確認できるようになるため非常に便利だ。

まとめ

今回は各ツールをインストールし、実行した結果を紹介した。それぞれのツールにはさまざまなオプションがあるので、より深い利用方法については各ツールの公式サイトや紹介サイトを参考にして欲しい。

次回は今回紹介できなかった他のツールについて紹介していきたい。(第4回 さまざまなジョブ(2) その他の抑えておきたいツールの紹介


社内サーバにリモートリポジトリを作るのも一つですが、「開発にまつわる面倒事」をこの際全部、tracpath(トラックパス)に任せてみませんか?
バージョン管理サービス・プロジェクト管理サービスの「tracpath(トラックパス)」では、
ユーザー5名、リポジトリ数3つまで、無料で利用可能です。

さっそく実務でも使って見ましょう。
自らも開発を行う会社が作ったからこそ、開発チームの「作る情熱」を支える、やるべきことに集中出来るサービスになっています。
エンタープライズ利用が前提のASPサービスなので、セキュリティも強固です。