DBUnit 使ってみた

最近、テストコードを書いているのですが、DB 周りのテストに大変苦戦しておりました。PHPUnit を使って書いているのですが、ただの Insert 文をテストするのにもかなり遠回りしなくちゃいけないことがあるんですよね。。。
今回は、そんな PHPUnit の拡張機能である DBUnit を使ってみます。

参考サイト:
https://phpunit-tenkoma-working.readthedocs.io/ja/latest/database/
https://www.ritolab.com/entry/168
https://qiita.com/takatama/items/63c7c82108af48b7bbdb

何ができるか

以下が DBUnit の特徴です。

  • DB の初期状態を別ファイルで定義して、テスト用の初期環境を作る。
  • 上記の別ファイルデータを、データセットやデータテーブルと呼ぶ。
  • アサーションで比較に使う値もデータセットで定義可能。
  • データセットは、XML、YAML、CSV、PHP の配列等々をサポート。
  • テーブルの行数カウントや、テーブルの情報自体を比較可能。
  • テスト実行後は、(特定のメソッドを定義すれば)自動で DB の変更を消去!

偉い感じしますね!

インストール方法

今回は、Composer を使ってインストールします。
アプリケーションのディレクトリで、下記コマンドを実行します。

composer require --dev phpunit/dbunit

同ディレクトリの、composer.json に下記記述が入っているはずです。

"require-dev": {
    "phpunit/dbunit": "^4.0"
}

これで DBUnit のインストールは完了です。

DBUnit 使い方

phpunit.xml に下記の記述をします。
ここでは、テスト用 DB に接続するための定数を定義しています。

<phpunit bootstrap="..\vendor\autoload.php">
  <php>
    <ini name="display_errors" value="on"/>
    <var name="DB_DSN" value="mysql:dbname=local_test;host=localhost" />
    <var name="DB_USER" value="root" />
    <var name="DB_PASSWD" value="" />
    <var name="DB_DBNAME" value="local_test" />
  </php>
</phpunit>

次に、DB 接続するメソッドを記述した、抽象クラスを定義します。
各テストコードは、このクラスを継承することになります。

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

abstract class Generic_Tests_DatabaseTestCase extends TestCase{
    use TestCaseTrait;

    // PDO のインスタンス生成は、クリーンアップおよびフィクスチャ読み込みのときに一度だけ
    static protected $pdo = null;

    // PHPUnit\Dbunit\Database\Connection のインスタンス生成は、テストごとに一度だけ
    private $connection = null;

    // DB接続
    final public function getConnection() {
        // TODO: Implement getConnection() method.
        if ($this->connection === null) {
            if (self::$pdo == null) {
                self::$pdo= new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']);
            }
            $this->connection = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
        }

        return $this->connection;
    }


    // DB のクリーンアップ
    public function getTearDownOperation() {
        return \PHPUnit\DbUnit\Operation\Factory::TRUNCATE();

    }

}

フィクスチャ用のデータセットを作ります。
今回は YAML ファイルを使います。

food:
  -
    id: 1
    name: karaage
    price: 450

それでは、テストコードを書きます。

<?php
require_once('Generic_Tests_DatabaseTestCase.php');
require_once('../../class/food.class.php');
use PHPUnit\DbUnit\DataSet\YamlDataSet;


class TestSample extends Generic_Tests_DatabaseTestCase {


    /**
     * フィクスチャ設定
     * @return \PHPUnit\DbUnit\DataSet\IDataSet|YamlDataSet
     */
    protected function getDataSet() {
        // TODO: Implement getDataSet() method.
        return new YamlDataSet(dirname(__FILE__).'\fixture_sample\sample.yml');
    }



    /**
    * テストコード
    */
    public function testGetDriverID() {


        // 初期状態の行数アサーション
        $this->assertEquals(1, $this->getConnection()->getRowCount('food'));

        // YAML ファイルで定義してあるデータから、アサーション用の比較データ取得。
        // 本来はフィクスチャ用のファイルと別に作るが、今回は Insert, Update, Delete を行わないため同じものを使用。
        $expectedTable = $this->getDataSet()->getTable('food')->getRow(0);


        // テストしたいメソッドの実行。
        $actual = Food::getById($expectedTable['id'], self::$pdo);


        // アサーション用のキー配列。created はテストしない。
        $check_keys_array = [
            'id', 'name', 'price'
        ];


        // 値が一致しているかのアサーション
        foreach ($check_keys_array as $key) {
            $this->assertEquals($expectedTable[$key], $actual[$key]);
        }

    }

}

このテストの実行はコマンドライン上で下記コマンドを打ってください。

phpunit -c phpunit.xml foodTest.php

以上でテスト完了です。
テストでテーブルに変更があっても変更したところを消去してくれるんで、かなーり便利だと感じました。

一応、注意しなければならないのが、PHPUnit としては DBUnit の開発を既に終えているそうです。
ですが、foak されたプロジェクト( https://github.com/kornrunner/dbunit )があるので、検討する価値はあるかなという感じです。

AWS EC2 不具合インスタンスの不具合調査備忘録

Auto Scalingが設定してあるインスタンスで深刻な不具合が発生すると、設定によっては、調査中に勝手にインスタンスがTerminateされてしまうことがあります。

これを回避する方法のメモ。乱文。

正しいやり方かはわからないけれど、現状のインスタンス規模なら適当。

前提

インスタンス数: 2台
Auto Scaling設定あり(=重篤な不具合を検知すると自動で代替インスタンスが立ち上がる)

インスタンスのスタンバイ状態を活用する

  1. インスタンスの内部をいじる場合は、まずインスタンスを「スタンバイ状態」にしておきます。
    スタンバイ状態にすれば、ロードバランサーがアクセス異常を検知しなくなるので、好きなだけゆっくりと調査ができます。
    1. Auto Scaling > [インスタンス]タブ
    2. 対象のインスタンスを選択し、 [操作] > [スタンバイに設定]
      • ※スタンバイ状態にすると、ロードバランサーはそのインスタンスにリクエストを投げなくなります(=インスタンスとしては稼働しているが、いないものとして扱われる)。
    3. このとき、代替インスタンスを自動で立ち上げるかを選択できます。
      • 立ててもいいですが、調査対応後の動作確認をする際に、当該インスタンス以外をすべてスタンバイに切り替えるので、可能な限り立ち上げない方が良いかもしれません。
      • ※スタンバイ状態に切り変えると、Auto Scalingの「希望インスタンス数」の値は自動で変動し、意図しないインスタンスの増減を防止します。
  2. 調査後、問題なさそうになったら、スタンバイ状態のインスタンスを「実行中」に設定します。
  3. 当該インスタンスの「ライフサイクル」が「実行中」に切り替わるのを待ちます。
  4. その後、調査対象のインスタンス以外のインスタンスを「スタンバイ」に設定し、動作検証を行いたいインスタンスのみがアクセスの対象となるようにします。
  5. 動作確認します。
  6. 問題なく稼働するようであれば、インスタンスのイメージを取得して保存します。
  7. 適当なインスタンス数になるよう調整したりなんだりします。
    • インスタンスの希望台数を適性に設定し直し、不要なインスタンスを終了させていく。
    • ※スタンバイ状態にしたままインスタンスがTerminateされると、Auto Scalingの[インスタンス]タブにゾンビ項目みたいに残ったりする?かもしれない。多分インスタンスが終了しても即座に紐付けが消失しないせい。
      (インスタンス一覧から消えればさすがに反映されると思われる)

CloudWatchのログを見る

CloudWatchにログを吐き出す設定をしていれば、ログが利用できるはずです。

  1. CloudWatch > 左側メニュー[ロググループ]
  2. ドメイン名などで検索をかけ、表示するログを絞ります。
  3. ログを開くと、インスタンスごとにファイルがわかれているので、問題が生じているインスタンスのIDで検索をかけてログファイルを特定します。
  4. ログを見ます。
    • ログの最下部にあるpauseを押せば、ログの自動更新を一時停止できます。
    • 時間を絞って表示することもできてまあまあ便利。

インスタンスのログを見る

Apache : /var/log/httpd/error_log とか。
nginx : /var/log/nginx/error_log とか。

参考

PHP コード内で PHPUnit を呼び出す(実行する)

PHPUnitを実行するPHPスクリプトを書きたい日がありました。
まあまあ時間が溶けたので備忘録です。

結論

このように書けばOK。

exec('/path/to/phpunit /path/to/tests');

参考: php – How to trigger unit testing from Laravel Artisan – Stack Overflow

特定のテスト単体を実行したい場合はこのように書けばOK。

exec('/path/to/phpunit /path/to/target /path/to/tests');

関数ごとの出力例をざっくり

記述法は結論の通り。

PHPには外部プログラムの実行ができる関数が色々あるので、出力結果を見比べてみました。

下記のようなコードで出力を確認しました。

$result = exec('/path/to/phpunit/ /path/to/tests');
var_dump(gettype($result));
var_dump($result);

exec()

成功時
var_dump(gettype($result));
string(6) "string"
var_dump($result);
string(24) "OK (1 test, 1 assertion)"
エラー発生時
var_dump(gettype($result));
 string(6) "string" 
var_dump($result);
 string(52) "Tests: 75, Assertions: 3650, Errors: 4, Failures: 8." 

エラー発生時に ERROR! の文言が出ないが、成功時はシンプルかつ OK と出るので良さそう。


shell_exec()

成功時
var_dump(gettype($result));
string(6) "string"
var_dump($result);
string(194) "PHPUnit 5.7.27 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 231 ms, Memory: 14.00MB OK (1 test, 1 assertion) "
エラー発生時
var_dump(gettype($result));
string(6) "string"
var_dump($result);
string(108797) "PHPUnit 5.7.27 by Sebastian Bergmann and contributors. (中略) ERRORS! Tests: 75, Assertions: 3650, Errors: 4, Failures: 8. "

OKERROR! の文言は出るものの、出力が長大で微妙……


system()

成功時
system()の出力(勝手に出る)
PHPUnit 5.7.27 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 229 ms, Memory: 14.00MB OK (1 test, 1 assertion)
var_dump(gettype($result));
string(6) "string"
var_dump($result); 
string(24) "OK (1 test, 1 assertion)"
エラー発生時
system()の出力(勝手に出る)
PHPUnit 5.7.27 by Sebastian Bergmann and contributors. (中略) ERRORS! Tests: 75, Assertions: 3650, Errors: 4, Failures: 8.
var_dump(gettype($result));
string(6) "string"
var_dump($result);
string(52) "Tests: 75, Assertions: 3650, Errors: 4, Failures: 8."

system()は実行結果をすべて出力するため、 var_dump(gettype($result)); の出力内容の前にPHPUnitの実行結果がドカドカ出てきてしまい、見にくかった。

var_dump($result);の出力結果は exec() と同じ。見やすくて良い。

ということで

PHPUnitを外部から実行し、出力内容を利用して成否判定するようなことができるようになりました。

system() は実行結果を出力するところまで行うため、PHPUnitの実行結果(最後の行)だけ見たい場合には、 exec()を使うのが良さそう。

参考

Vagrant CentOS6 PHP5.6 → 7.2 バージョンアップ

ローカル開発環境のPHPが5.6だったので、バージョンを上げてみようと思いました。

CentOSにPHP7.2入れたい
 →Remiというプロジェクトのリポジトリを使うとyumで入れられるらしい
  →Remiを利用するためにはEPELリポジトリのインストールも必須要件

手順

root権限を持つユーザで作業します。

EPELリポジトリのインストール

# yum -y install epel-release

えーなんか出た……

[root@localhost ~]# yum -y install epel-release
Geladene Plugins: fastestmirror
Einrichten des Installationsprozess
Loading mirror speeds from cached hostfile
Could not get metalink https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=x86_64 error was
14: problem making ssl connection
 * base: ftp-srv2.kddilabs.jp
 * epel: ftp.riken.jp
 * extras: ftp-srv2.kddilabs.jp
 * remi-safe: ftp.riken.jp
 * updates: ftp-srv2.kddilabs.jp
Paket epel-release-6-8.noarch ist bereits in der neusten Version installiert.
Nichts zu tun

yumの設定を書き換えて対処。
baseurl のコメントアウトを外して、 mirrorlist をコメントアウト。
参考: yumがepelのmirrorにつながんねーよと怒ってる時の対処法 – Qiita

# vim /etc/yum.repos.d/epel.repo
[epel]
name=Extra Packages for Enterprise Linux 6 - $basearch
#baseurl=http://download.fedoraproject.org/pub/epel/6/$basearch
mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=$basearch
failovermethod=priority
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-6

[epel]
name=Extra Packages for Enterprise Linux 6 - $basearch
baseurl=http://download.fedoraproject.org/pub/epel/6/$basearch
#mirrorlist=https://mirrors.fedoraproject.org/metalink?repo=epel-6&arch=$basearch
failovermethod=priority
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-6

再実行

# yum -y install epel-release
[root@localhost ~]# yum -y install epel-release
Geladene Plugins: fastestmirror
Einrichten des Installationsprozess
Loading mirror speeds from cached hostfile
 * base: ftp-srv2.kddilabs.jp
 * extras: ftp-srv2.kddilabs.jp
 * remi-safe: ftp.riken.jp
 * updates: ftp-srv2.kddilabs.jp
epel                                                         | 4.7 kB     00:00
epel/primary_db                                              | 6.1 MB     00:02
Paket epel-release-6-8.noarch ist bereits in der neusten Version installiert.
Nichts zu tun

ヨシ!

Remiリポジトリのインストール

# yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
[root@localhost ~]# yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
Geladene Plugins: fastestmirror
Einrichten des Installationsprozess
remi-release-7.rpm                                                                                               |  20 kB     00:00
Untersuche /var/tmp/yum-root-KfXqbk/remi-release-7.rpm: remi-release-7.7-2.el7.remi.noarch
Markiere /var/tmp/yum-root-KfXqbk/remi-release-7.rpm als eine Aktualisierung für remi-release-6.9-2.el6.remi.noarch
Loading mirror speeds from cached hostfile
 * base: ftp-srv2.kddilabs.jp
 * extras: ftp-srv2.kddilabs.jp
 * remi-safe: ftp.riken.jp
 * updates: ftp-srv2.kddilabs.jp
Löse Abhängigkeiten auf
--> Führe Transaktionsprüfung aus
---> Package remi-release.noarch 0:6.9-2.el6.remi will be aktualisiert
---> Package remi-release.noarch 0:7.7-2.el7.remi will be an update
--> Verarbeite Abhängigkeiten: epel-release = 7 für Paket: remi-release-7.7-2.el7.remi.noarch
--> Abhängigkeitsauflösung beendet
Fehler: Package: remi-release-7.7-2.el7.remi.noarch (/remi-release-7)
            Requires: epel-release = 7
            Installiert: epel-release-6-8.noarch (installed)
                epel-release = 6-8
 Sie können versuchen --skip-broken zu benutzen, um das Problem zu umgehen.
 You could try running: rpm -Va --nofiles --nodigest

ヨシ!

Remiのインストール状況を確認

# ls -l /etc/yum.repos.d/
[root@localhost ~]# ls -l /etc/yum.repos.d/
insgesamt 64
-rw-r--r--. 1 root root 1991 23. Okt 2014  CentOS-Base.repo
-rw-r--r--. 1 root root  647 23. Okt 2014  CentOS-Debuginfo.repo
-rw-r--r--. 1 root root  289 23. Okt 2014  CentOS-fasttrack.repo
-rw-r--r--. 1 root root  630 23. Okt 2014  CentOS-Media.repo
-rw-r--r--. 1 root root 5394 23. Okt 2014  CentOS-Vault.repo
-rw-r--r--  1 root root  957 22. Jun 18:47 epel.repo
-rw-r--r--. 1 root root 1056  5. Nov 2012  epel-testing.repo
-rw-r--r--  1 root root 1209  2. Dez 2013  mysql-community.repo
-rw-r--r--  1 root root 1060  2. Dez 2013  mysql-community-source.repo
-rw-r--r--. 1 root root  456 16. Jan 2018  remi-php54.repo
-rw-r--r--. 1 root root 1314 16. Jan 2018  remi-php70.repo
-rw-r--r--. 1 root root 1314 16. Jan 2018  remi-php71.repo
-rw-r--r--. 1 root root 1314 16. Jan 2018  remi-php72.repo
-rw-r--r--. 1 root root 2605 16. Jan 2018  remi.repo
-rw-r--r--. 1 root root  750 16. Jan 2018  remi-safe.repo

epel、remiともにインストールできていることがわかる。
特に、remi-php72.repoが取得できていることが確認できる。

ここまでが下準備。

古いPHPのアンインストール

インストール済のPHP関連のパッケージ一覧を表示

# yum info installed php*

ずらずらっと色々出てくる。

アンインストール

# yum remove php*
~~(php関連パッケージたち)~~

========================================================
Remove       93 Package(s)

Installed size: 95 M
Ist dies in Ordnung? [j/N] :j

~~(削除中)~~

Komplett!
[root@localhost ~]#

消えていることを確認

[root@localhost ~]# php -v
-bash: /usr/bin/php: Datei oder Verzeichnis nicht gefunden

新しいPHPのインストール

# yum install --disablerepo=* --enablerepo=epel,remi,remi-safe,remi-php72 php

インストール完了後、バージョン確認。

[root@localhost ~]# php -v
PHP 7.2.31 (cli) (built: Jun  9 2020 08:25:54) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies

<関係ないですが、ここで言語設定をドイツ語から英語に変更しました>

phpMyAdminのインストール

# yum install -y --enablerepo=remi-php72 phpMyAdmin

なんか言われた。

Error: Package: phpMyAdmin-4.0.10.20-1.el6.noarch (epel)
           Requires: php(language) < 7
           Installed: php-common-7.2.31-2.el6.remi.x86_64 (@remi-php72)
               php(language) = 7.2.31
           Available: php-common-5.3.3-49.el6.x86_64 (base)
               php(language) = 5.3.3
           Available: php-common-5.3.3-50.el6_10.x86_64 (updates)
               php(language) = 5.3.3
           Available: php-common-7.2.31-1.el6.remi.x86_64 (remi-php72)
               php(language) = 7.2.31
 You could try using --skip-broken to work around the problem
 You could try running: rpm -Va --nofiles --nodigest

phpMyAdminが入らないのはアカン。

対処します。
参考: centos – centOS6.8にphpMyAdminをyumでインストールしたさいのエラー – スタック・オーバーフロー

remiリポジトリのアップデート

# yum --disablerepo=* --enablerepo=remi,remi-php72 update

remiのみを指定して再インストール

# yum --disablerepo=* --enablerepo=remi,remi-php72 install phpMyAdmin

ずらずらっと処理が走って Complete! したのでOK。

Apacheの再起動

# service httpd restart

phpMyAdminが無事開けたのでOK

と思いきや、いざlocalを開いてみたらまたエラー

やれやれ

Notice: Undefined variable: u_id in (ファイルのパス) on line 78

Notice: Undefined index: action in (ファイルのパス) on line 11

Warning: require_once(hogefuga.php): failed to open stream: No such file or directory in (ファイルのパス) on line 4

Fatal error: require_once(): Failed opening required 'hogefuga.php' (include_path='.:/usr/share/pear:/usr/share/php') in (ファイルのパス) on line 4

.htaccessを確認

<IfModule mod_php5.c>
 php_value include_path "hogehogehogehogehohgeohgoehogehogheogehoge"
 php_value error_reporting 6135
 php_value mysql.connect_timeout 1
</IfModule>

あーーーーーーー!

対処

php_value include_path "hogehogehogehogehohgeohgoehogehogheogehoge"
php_value error_reporting 6135
php_value mysql.connect_timeout 1

一般的に、<IfModule>使うのはよろしくないという言説も見かけるので削除。

再びのApache再起動

# service httpd restart

きちんとページが表示されたのでヨシ!

参考

AWS S3アップロード用のIAMユーザーを作成する

プログラムからファイルをアップロードする用のユーザー登録を駆け足で。

1. IAMダッシュボードを開く

AWSマネジメントコンソールへログイン後、Identity and Access Managementへアクセスします。

2. ユーザーの登録

1. 画面左の[ユーザー]をクリックします。

2. 画面上部の[ユーザーを追加]をクリックします。

青い[ユーザーを追加]をクリックする。

3. 作成の流れ

1. 名称とか適当に入れます。
今回はプログラムからのアクセス用IAMを作ります。
2. S3FullAccessの権限を与えます。
3. タグはお好きに。
4. 設定内容を確認。
5. 作成すると、アクセスキー等が表示されます。
シークレットアクセスキーの[表示]をクリック。
6. シークレットアクセスキーが表示されるのでコピーしてコードに貼っ付けるなりします。
後から確認はできないっぽいので、csvダウンロードしておくのも良いかも。
7. hogeが作成されてる。
8. シークレットアクセスキーがわからなくなった等あれば、
アクセスキーを再生成してしまうのも手。

以上。

あとはプログラムでS3にアクセスするときの値に入れるだけです。
s3にファイルをアップロードする処理内で、例えば下記のように設定します。

AWS_ACCESS_KEY = 'XXXXXXXXXXXXXXXXXXXX';
AWS_SECRET_ACCESS_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
AWS_S3_BUCKET_NAME = 'test_bucket';


function upload_to_s3($upload_file) {
    // S3clientのインスタンス生成
    $s3client = new Aws\S3\S3Client([
        'credentials' => [
            'key' => AWS_ACCESS_KEY,
            'secret' => AWS_SECRET_ACCESS_KEY
        ],
        'region' => 'ap-northeast-1',
        'version' => 'latest'
    ]);
    
    // 画像のアップロード
    $s3client_image_upload = $s3client->putObject([
        'ACL' => 'public-read',
        'Bucket' => AWS_S3_BUCKET_NAME,
        'Key' => $upload_file['file_name'],
        'SourceFile' => $upload_file['file_path'],
        'ContentType' => mime_content_type($upload_file['file_path'])
    ]);
        
    // アップロードファイルのURL取得
    return $s3client_image_upload['ObjectURL'];;
}