はじめに
こんにちは。asken エンジニアの羽鳥です。
今回は CakePHP のユニットテストにてハマった問題について記事にしました。
問題解決にあたっていくつか学びもありましたので、どこかの誰かのお役に立てれば幸いです。
前置き
注意点
- 本記事内のコードは実際にシステムで使われているコードではございません
- 今回の記事用に作成したコードを掲載しております
- CakePHP のユニットテストそのものの説明は省かせていただきます
システム前提
- PHP
- CakePHP
- phpunit
- DB : MySQL
起きていた問題!
以下のような Fixture があったとします。
/** * 体重計測結果の管理テーブル */ class WeightFixture extends CakeTestFixture { public $fields = [ 'id' => ['type' => 'integer', 'key' => 'primary'], 'weight' => ['type' => 'float', 'null' => false, 'comment' => '体重(kg)'], 'recorded' => ['type' => 'datetime', 'null' => false, 'comment' => '計測日時'], ]; public $records = [ [ 'id' => 1, 'weight' => 52, 'recorded' => '2022-06-01 10:39:23', ], [ 'id' => 2, 'weight' => 55.3, 'recorded' => '2022-06-01 11:42:19', ], ]; }
このとき、本来であれば以下のようにデータが登録されていてほしいのですが、
id | weight | recorded |
---|---|---|
1 | 52.0 | 2022-06-01 10:39:23 |
2 | 55.3 |
2022-06-01 11:42:19 |
以下のようにデータが登録されてしまうという現象が起きておりました。
id | weight | recorded |
---|---|---|
1 | 52.0 | 2022-06-01 10:39:23 |
2 | 55.0 |
2022-06-01 11:42:19 |
つまり、Fixture 側の「fields プロパティ」には「float」で定義されているのにも関わらず、なぜか実際にデータベースにテストデータが投入された際には、「int」で丸められた値が登録されてしまうという問題が起きていました。
なぜだ、なぜなんだ......。
原因はコイツだ!
原因をヒトコトで
原因についてヒトコトで整理するならば以下の通りです。
- 「records プロパティ」の先頭1件目で値を「int」として解釈される値で記述していること
では、なぜ先頭1件目を「int」として解釈される値で記載すると今回の事象が起きるのでしょうか?
それは、フレームワーク側がどのようにテストデータを登録しているのか解読すると見えてきます。
CakePHP2系はテストデータをどのように登録しているのか?
CakePHP2系では、Fixture 内の「records プロパティ」を使用してテストデータを投入しています。
その際の挙動を抜粋して記載すると以下の通りです。
- lib/Cake/TestSuite/Fixture/CakeTestFixture.php
- 「insert メソッド」内で DboSource オブジェクトの「insertMulti メソッド」を呼び出す
- lib/Cake/Model/Datasource/Database/Mysql.php
- 「insertMulti メソッド」内で継承元の「introspectType メソッド」を呼び出す
- このとき「record プロパティ」の
先頭1件目のデータのみ
を使用する - カラム名をキーに、型判定結果をバリューとして連想配列に保存する (a)
- lib/Cake/Model/Datasource/DboSource.php
- 「introspectType メソッド」内で型を判定を行う
- lib/Cake/Model/Datasource/Database/Mysql.php
- 「bindValue メソッド」の第3引数に (a) の情報を設定してクエリを作成する
- テストデータを insert するクエリを実行する
そのため、本来 float として登録してほしいデータを「records プロパティ」の 先頭1件目で「int」として記載してしまうと、先頭1件目以降すべてのデータについても同様に「int」として登録されてしまいます。
さらに調べると...
尚、この現象が発生するのは「PHP 7.2 以降」となる可能性が高いです。
まさかの PHP バージョンによって挙動が異なる可能性があるのです。
PDOStatement::bindValue の「type」パラメータには「PDO::PARAM_* 定数」が指定可能です。この「PDO::PARAM_* 定数」の挙動が「PHP 7.2」以降のバージョンで異なるという記事を見つけました。
- Doc Bug #77954 Binding value as PDO::PARAM_INT has different behaviour in PHP 7.2
- PHP 7.2以降におけるPDO::PARAM_INTの仕様変更
- あの PDO::PARAM_INT の挙動が変わった!
この記事の通りであれば、以下のような挙動の差異があることになります。
- PHP 7.2 未満
- 「bindValue メソッド」は「PDO::PARAM_INT」指定時に値を「int」にキャストしない
- PHP 7.2 以降
- 「bindValue メソッド」は「PDO::PARAM_INT」指定時に値を「int」にキャストする
言語やフレームワークのバージョンアップが一筋縄では行かないことがよくわかる例ですね(汗)
気をつけなければならないこと
時に予期せぬ挙動をする可能性があるため、細部に気を使って確認する必要があることを身をもって体験したケースでした。
また、以下の順で調査をしていったのですが、やはり困ったときはフレームワーク側のコードを読むことが確実でした。
- 似た事象で悩んでいる人がいないかググる
- CakePHP の公式リファレンスを読む
- CakePHP の github 内で issue, PR を検索する
- フレームワークのソースコードを読む
普段のコーディングに関して言えば、はじめから横着せずに float と解釈される値を記載しておけば良かったとも考えられます。今回のコードで考えると「52」ではなく「52.0」と記載しておけば良かったということになります。
確かに「52」と書いて済ませたい気持ちもわかります。ですが、こうして地味にハマるような問題が発生するようにもなります。今回のような問題が発生することもあるため、何事も意識的に気をつけて実装した方がベターだというまとめになります。
お知らせ
askenでは、問題が発生したときにワクワク解決に向かうエンジニアを募集しています。