概要
Controller のテストにおいて、IntegrationTestTrait(IntegrationTestCase)の get / posts といったメソッドを利用する機会は多いと思います。
この「POST や PUT のリクエスト」において、ファイルアップロード処理についてはどのように扱うべきでしょうか?
post()に渡すデータ = リクエストボディとなるデータとは別に、configRequest()によるリクエストコンテキストへのアップロードファイル情報の注入が必要です。
イントロ
CakePHP のコントローラーのテストは、IntegrationTestTrait(以前は IntegrationTestCase)を利用することで、「任意の HTTP メソッドで」「特定の URL に、query や payload 込みでリクエストした場合」をエミュレーションし、かつ「Response Header」「ResponseBody(描画される内容や view 変数にセットされた値)」を検査するテストが実装できます。
その内容は、CakeBook のコントローラーの統合テストに詳しいです。
これは内部的には ServerRequest クラスとうまく協調することで実現されていますので、「ファイルのアップロード」に関しても再現することが可能です。
これは Book 上では詳細な言及がなかったので、調べてみました。
IntegrationTestTrait での「ファイルのアップロード」
IntegrationTestTrait を利用した基礎的なリクエストの例
IntegrationTestTrait での「リクエストのテスト」のためのメソッドには、以下のようなものがあります。
- get()
- head()
- options()
- patch()
- post()
- put()
Trait Cake\TestSuite\IntegrationTestTrait | CakePHP 3.7
このうち patch post put は第 2 引数をとり、リクエストコンテンツ(BODY)を渡すことが可能です。
ざっくりといって、 PHP でいえば $_POST に入ってくる内容をセットできるのだとイメージしてください。
$this->post('/api/post/add.json', $data);
ServerRequest と \$_FILE
ここで、少しだけ詳しく「post() の中で何が行われているのか」を見てみましょう。
簡単にまとめると
- リクエスト内容を組み立てて
- それを
ServerRequestに変換し ServerRequestを利用して、実際のリクエスト処理の実行(Server= Middleware のスタックを順に処理する)- 実行結果をセットする
という内容が取り扱われています。
今回の関心領域で言えば、「ServerRequest はどのように組み立てられるのか?」という部分になります。
それをサマリーとして表したのが、以下のシーケンス図です。

ここから、 ServerRequest インスタンスはServerRequestFactory::fromGlobals() というメソッドを介して取得されていることがわかります。
ServerRequestFactory::fromGlobals() は、「 PHP のスーパーグローバル変数($_FILES, $_COOKIE, $_GET, $_POST)と明示的に渡された各引数を合成して、 ServerRequest(PSR-7)のインスタンスを組み立てる、というものです。
public static function fromGlobals(
array $server = null,
array $query = null,
array $body = null,
array $cookies = null,
array $files = null
)
API: ServerRequestFactory::fromGlobals()
さて、「 IntegrationTestTraitの各種リクエストメソッドは、 HTTP メソッド名・リクエスト先 URL 情報、リクエストボディをとる」ということを想起してください。
すなわち $server $cookies $files は、別の手段を用いて渡して上げる必要がありそうです。
IntegrationTestTrait::configRequest()
ここで登場するのが、 IntegrationTestTrait::configRequest()というメソッドです。
これは、「次に実行されるリクエストのエミュレーションの内容を設定する」というものです。
/**
* Configures the data for the *next* request.
*
* This data is cleared in the tearDown() method.
*
* You can call this method multiple times to append into
* the current state.
*
* @param array $data The request data to use.
* @return void
*/
public function configRequest(array $data)
例えば、CakeBook を参照すると「リクエストヘッダーをセットする」といった用途で利用する例を紹介しています。
// ヘッダーの設定
$this->configRequest([
'headers' => ['Accept' => 'application/json']
]);
Book: リクエストの設定
このメソッドに渡された $data は、一時的に TestCase クラス(IntegrationTestTrait)のインスタンスメンバとして保持されることになり、最終的に dispatcher に渡される「リクエスト情報」として URL やリクエストボディとマージされることになります。
いいかえると、configRequest() に files というキーで渡された値が、 ServerRequestFactory::fromGlobals() に $files(第 5 引数)として渡されていくことになります。
これで準備が整ったので、実際にファイルアップロードを試してみましょう。
結合テストでファイルアップロードを試す
やることは 2 つです。
- 実際に
$_FILESに入ってくるような形式の連想配列を用意する - その内容を、
configRequest()とpost()に同一のキーで渡す
データ形式
利用する値は、 error tmp_name size というキーを持つ連想配列である必要があります。
これは、IntegrationTestTraitがリクエストデータを組み立てるときに簡易なバリデーションを行っていて、その条件を満たさなければ「配列でなくてスカラデータとして扱えるように変換する」という機構になっているためです。
非常にシンプルな実装なので、実コードを示します。
if (is_array($value)) {
$looksLikeFile = isset($value['error'], $value['tmp_name'], $value['size']);
if ($looksLikeFile) {
continue;
}
$data[$key] = $this->_castToString($value);
}
Source: IntegrationTestTrait
コードサンプル
//$srcはCake\Filesystem\Fileのインスタンスなどを想定しています
$file = [
'error' => 0,
'name' => $src->name,
'size' => $src->size(),
'tmp_name' => $src->path,
'type' => $src->mime(),
];
$this->configRequest([
'Content-Type' => 'multipart/form-data',
'files' => [
'field-name' => $file,
]
]);
$this->post(
'/api/add',
[
'field-name' => $file,
]
);
このコードがあれば、ServerRequest::getUploadedFile('field-name') (コントローラー内部からであれば $this->getRequest()->getUploadedFile('field-name'))によるファイルへのアクセスを伴うコードもテストが可能になります。
API: getUploadedFile()
まとめ
ファイルアップロードのテストはやりづらさを感じる場面が多いものと思いますが、CakePHP ではこうすることでグローバル変数を汚染することなく検査ができることがわかりました。
少々の冗長さも感じるので、もしファイル関連の操作を頻繁に行うアプリケーションであるなら、TestSuite やそれに類するレイヤーに「ファイルのアップロードのテストを行う」ためのヘルパーを用意しても、便利かもしれません。