解決したいこと
テストしたいメソッドの中にある、Eloquentの挙動をMockしたい。
ユニットテストは初めての挑戦で、そもそも考え方に間違いがあるかもしれません。。
以下は色々チャレンジしてみた結果になりますが、
要するにテスト時は処理速度の観点から簡易的な配列を使ってテストをすることで確認を高速化したいし、
エラー側のテストとしても、Eloquent呼び出しにへんな値を差し込めるようにしたいけど
そもそもEloquentのモック自体ができないので困っている、といったような内容です。
Laravelのテストに関する記事はFeatureに関するものばかりで、ユニットテスト系があんまり見つけられなかったので
こちらに直接質問させていただこうと思いました。
どうぞよろしくお願いいたします。
具体的な内容
次のようなコードがあるとします。
php:HogeController
1HogeController extends Controller{ 2 function HogeFunction(){ 3 //1. レコード全体を取得 4 $records = HogeModel::with(['example', 'relation', 'tables'])->get(); 5 foreach($records as $record){ 6 ...固有の処理... 7 //2. Hogeから取得した値を用いてFugaを検索 8 $id = FugaModel::where('id', '=', $record->id)->get(); 9 //3. この処理では通常通りの読み出しを行いたい 10 $value = HogeModel::where('foo', '=', 'bar')->get(); 11 ...固有の処理... 12 } 13 } 14}
このとき、1.では全てのレコードを取得したくないし、
2.ではandReturn
を使った規定値を返したい
また、3.ではHogeModelの動作を上書きせず、通常通りの挙動を期待しています。
実際に試してみた内容
1や2のような静的メソッドをMockeryでモックする場合は、alias
またはoverride
を使わないといけない
3のような、一部だけモックして他メソッドは通常通りの挙動を期待する場合はmakePartial
を使わないといけない
ということで、次のようなテストコードを書きました。
php:testHogeClass
1function testHogeFunction() 2{ 3 //テストコード実行時期待される返り値 4 $hogeAssert = 'fugafuga'; 5 6 //1. andReturnで返す値を1レコードのみにするために、先に値を取得しておく 7 $dummyHoge = \App\HogeModel::with(['example', 'relation', 'tables']) 8 ->where('id', '=', 1) 9 ->first(); 10 $dummyFuga = 'piyopiyo'; 11 12 //1.および3. MakePartialを使い、またshouldReceiveにメソッドチェーンを記述することで、 13 // Mockされる部分を限定的に指定する。(3.のwhere->getや、$dummyHogeのwhere->firstに影響を及ぼさない) 14 $hogeMock = \Mockery::mock('alias:' . \App\Hoge::class)->makePartial(); 15 $hogeMock->shouldReceive('with->get')->andReturn($dummyHoge); 16 17 //2. FugaModelの返り値をモックする 18 $fugaMock = \Mockery::mock('alias:' . \App\Fuga::class)->makePartial(); 19 $fugaMock->shouldReceive('where->get')->andReturn($dummyFuga); 20 21 //テストしたいメソッドの実行 22 $hoge = new HogeController(); 23 $this->assertEquals($hogeAssert, $hoge->HogeFunction()); 24}
クラスの重複エラー
しかしこの時、
bash
1There was 1 error: 2 31) Tests\Unit\HogeTest::testHogeFunction 4Mockery\Exception\RuntimeException: Could not load mock App\Hoge, class already exists 5
というエラーが出てきます。
$hogeMock
が行なっているクラスのモックと$dummyHoge
が行なっているような元のクラスの呼び出しが共存すると
同名のクラスが重複してしまうことでエラーが起きてしまうといったことのようです。
しかしながら、Eloquentは静的クラスですし、グローバルスコープにある以上、
たとえ__construct
でインスタンス作成時に参照するEloquentを、元のクラス/モックと選択式に注入できるようにしたところで
結局実行されるコードには\App\HogeModel
といったコードが記述される以上
EloquentのmakePartialなクラスの上書き(=モック化)はできないような気がしています。
php:HogeController
1protected $hogeObj; 2function __construct($model = null){ 3 if($model){ 4 $this->hogeObj = $model; 5 }else{ 6 //ここで\App\HogeModelを宣言している以上、テストメソッド側で何を書こうが 7 //\App\HogeModelは全てモックしたクラスに上書きされてしまう? 8 $hits->hogeObbj = new \App\HogeModel(); 9 } 10}
makePartialの失敗エラー
対応策として考えたのは、中身は同じだが名前空間の違うmodelを定義し、(たとえば\App\HogeModel
と、\App\TestModel\HogeModel
)
テスト記述時に先に値を取得しておきたい時は\App\TestModel\HogeModel
を使用....でしょうか。
または、
$dummyHoge = new \App\HogeModel; $dummyHoge->id = 1; $dummyHoge->value = hogehoge; ....
と、手でEloquentモデルを先に作っておいても良いかもしれません。
この方法でEloquentを用いず、andReturnを手で定義し、
HogeModel
のwith->get
をmakePartial
してみました。
php
1function testHogeFunction() 2{ 3 //テストコード実行時期待される返り値 4 $hogeAssert = 'fugafuga'; 5 6 //1. andReturnで返す値を1レコードのみにするために、先に値を取得しておく 7 $dummyHoge = $this->getdummyHoge() 8 $dummyFuga = 'piyopiyo'; 9 10 $hogeMock = \Mockery::mock('alias:' . \App\Hoge::class)->makePartial(); 11 $hogeMock->shouldReceive('with->get')->andReturn($dummyHoge); 12 //...以下同じ 13} 14 15function getdummyHoge() 16{ 17 $response = new \App\HogeModel(); 18 $response->id = 1; 19 $response->value = 'hogehoge'; 20 ... 21 return $response; 22}
結果
bash
1BadMethodCallException: Static method App\HogeModel::where() does not exist on this mock object
とのエラーが出て、
テストしたいメソッドの中のHogeModel::with()->get()
は確かにモックされたようですが、
HogeModel::where()->get()
は上書きされなかったようです。
結局MakePartial
が動いていないといった意味になるかと思います。
結果
静的クラスをモックすると、グローバルをモッククラスで汚染してしまうということが主な原因かと思いますが、
同時に、Eloquentは静的クラスにしかなり得ないんじゃないのかなと思います。
同じ処理を行う、名前空間の異なるモデルを2つ用意する、だったり
$this->getdummyHoge()
のような、戻ってくる値を手で作っておくというやり方は
CI的にも、スキーマ変更時の対応だったり、後々の修正としても、あまりやるべきではない行為だと思います。
Factory/Fakerを使う記事はたくさんあるのですが、assertDatabaseHas
のように、
DB書き込みが前提になっているものばかりで、
create
やupdate
ではなく、データベースの値を使った独自の処理だったり、
Faker向きではない、ステータス情報などの固定されたDBテーブルを使った処理だったりには
それらの内容は不向きでしたし、それらの情報を持って応用できるものではありませんでした。
スマートにEloquentの挙動を部分部分で上書きできればそれでいいのですが、
なんか遠回りしてしまっているようで....
色々考え方自体が間違っていたらすみません。
どうぞよろしくお願いいたします。
回答1件
あなたの回答
tips
プレビュー
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/08/20 12:06
2018/08/20 12:07
2018/08/21 00:23
2018/08/21 10:59