質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

PHPUnit

PHPUnitは、PHP向けのユニット・テスト向けフレームワークで、手動では手間のかかるテスト作業を自動化し、繰り返し実行することが可能です。

ユニットテスト

ユニットテストは、システムのテスト手法の一つで、個々のモジュールを対象としたテストの事を指します。対象のモジュールが要求や性能を満たしているか確認する為に実行します。

Eloquent

Eloquentとは、PHPフレームワークのLaravelに最初から含まれているORM(Object-relational mapping:オブジェクト関係マッピング)です。

Q&A

解決済

1回答

6537閲覧

Eloquentを使った関数のテストができない

yokatone

総合スコア43

Laravel

LaravelとはTaylor Otwellによって開発された、オープンソースなPHPフレームワークです。Laravelはシンプルで表現的なシンタックスを持ち合わせており、ウェブアプリケーション開発の手助けをしてくれます。

PHPUnit

PHPUnitは、PHP向けのユニット・テスト向けフレームワークで、手動では手間のかかるテスト作業を自動化し、繰り返し実行することが可能です。

ユニットテスト

ユニットテストは、システムのテスト手法の一つで、個々のモジュールを対象としたテストの事を指します。対象のモジュールが要求や性能を満たしているか確認する為に実行します。

Eloquent

Eloquentとは、PHPフレームワークのLaravelに最初から含まれているORM(Object-relational mapping:オブジェクト関係マッピング)です。

0グッド

1クリップ

投稿2018/08/19 13:55

編集2018/08/19 13:59

解決したいこと

テストしたいメソッドの中にある、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を手で定義し、
HogeModelwith->getmakePartialしてみました。

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書き込みが前提になっているものばかりで、
createupdateではなく、データベースの値を使った独自の処理だったり、
Faker向きではない、ステータス情報などの固定されたDBテーブルを使った処理だったりには
それらの内容は不向きでしたし、それらの情報を持って応用できるものではありませんでした。

スマートにEloquentの挙動を部分部分で上書きできればそれでいいのですが、
なんか遠回りしてしまっているようで....

色々考え方自体が間違っていたらすみません。
どうぞよろしくお願いいたします。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

上記の様な複雑なモックを考えるのであれば、見方を変えて他のメソッドでWrapしてしまった方が楽なのでは?と感じました。
例えばですが、下記の様な考え方は如何でしょうか?
(設計を根本から変えてしまうので、難しかったらごめんなさい)

本番で動作するControllerは、下記の様なInterfaceとその実装を作ってInterfaceをインジェクション。

HogeInterface

php

1<?php 2namespace App; 3 4interface HogeInterface{ 5 function getAll(); 6 7 function getByFoo($key); 8}

HogeClass

php

1<?php 2namespace App; 3 4class HogeClass implements HogeInterface{ 5 public function getAll() 6 { 7 return HogeModel::with(['example', 'relation', 'tables'])->get(); 8 } 9 public function getByFoo($key) 10 { 11 return HogeModel::where('foo', '=', 'bar')->get(); 12 } 13}

HogeController

php

1 /** 2 * HogeInterfaceをインジェクション 3 * (ServiceProviderでbind) 4 * @return \Illuminate\Http\Response 5 */ 6 public function index(HogeInterface $hoge) 7 { 8 $records = $hoge->getAll(); 9 $value = $hoge->getByFoo('bar'); 10 return $hoge; 11 }

テストの時にはそれ用メソッドを作って確認

テスト用メソッド(HogeInterfaceを実装)

php

1<?php 2namespace App; 3 4class TestHoge implements HogeInterface{ 5 public function getAll() 6 { 7 return ['A','B','C']; 8 } 9 public function getByFoo($key) 10 { 11 return HogeModel::where('foo', '=', 'bar')->get(); 12 } 13}

Controllerのテスト

php

1<?php 2 3namespace Tests\Unit; 4 5use Tests\TestCase; 6use Illuminate\Foundation\Testing\RefreshDatabase; 7use App\Http\Controllers\HogeController; 8 9class ExampleTest extends TestCase 10{ 11 /** 12 * A basic test example. 13 * 14 * @return void 15 */ 16 public function testBasicTest() 17 { 18 $test = new HogeController(new HogeTest()); 19 $this->assertTrue(true); 20 } 21}

投稿2018/08/20 09:27

motuo

総合スコア3027

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yokatone

2018/08/20 12:06

ORMを使っている部分を全てORM用のクラスでラッパし、 テスト/通常でインジェクションする対象を変えるということですね。 Eloquentを用いたメソッドのテスト、、というか、関数入り混じりの関数のテストの手法として このやり方が一般的なんでしょうか?(interfaceを使って、実際に使うクラスを場面に応じて切り替える) 今回のテスト作成が私に取って初めてのテスト構築なので、 よければ教えていただければ幸いです。 (今後の開発の参考にしたいと思います。)
yokatone

2018/08/20 12:07

あ、すみません。お礼を忘れておりました。 まずはこういった解決方法を教えていただきありがとうございました。 newするインスタンスに__constructを設けてどうにかする....という発想しかなかったため、 interfaceで対応するというのは大変参考になりました。ありがとうございます。
motuo

2018/08/21 00:23

> このやり方が一般的 テスト手法というより、設計の考え方の提示ですね。 全てのプロジェクトがこうです、とは言い切れませんがある程度、浸透している概念です。 「Laravel リポジトリパターン」とかで検索すると、良い情報がネット上にたくさんあります。 私が業務でLaravelを使うときも、ContorllerからModelを使う事は禁止しています。 データアクセスをするロジックと、Controllerの責任を明確に分離したいからです。 また、この方がテストを書きやすい、という効果もあります。
yokatone

2018/08/21 10:59

検索してみました。大変勉強になりました。 手間はかかりますが、確かにリポジトリパターンを採用したほうがすっきりしますね! また、MockeryはFacadesと連携した機能となっているが、 EloquentはFacadesを経由していないので、モックできないため Eloquentのモックを行うときは、リポジトリパターンを使う事が通例であるという事もわかりました。 ありがとうございました :D
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問