30
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

初めてのフレームワーク-Laravel5-7 テスト関連メモ

Last updated at Posted at 2016-09-23

#Laravel & Markdown記法の勉強及びメモとして書いています。
間違えている点ございましたら、ご指摘いただけますと大変ありがたいです。よろしくお願いいたします。

##テストサンプル

laravelでは、testフォルダ内に、テストファイルを書いていきます。
今回は下みたいに作った。

.
└── tests
    ├── Models - モデルクラスのテスト書く場所
    │   └── UserTest.php - UserModelのテスト
    ├── TestCase.php - 共通処理を書く。
    └── AjaxTest.php - Ajaxアクセスした時のテスト

###作業の流れ
####名前空間の整理
下記をcomposer.jsonに追加して、
composer dump-autoload
とすることで、testフォルダ下にnamespaceが使えた。

composer.json
    "autoload-dev": {
        "classmap": [
            "tests/TestCase.php"
        ],
        "psr-4": {
            "Test\\": "tests/"
        }
    },

####TestCase.phpの作成
TestCase.phpには、テストメソッド実行時に行われる共通処理を書くようです。
いろいろなところで使うFacadeをモックするメソッドを書いたら便利でした。

test/TestCase.php
    // これをtrueにしたら、毎回データベースがリセットされる。
    protected $resetDatabase = false;

    /**
     * Creates the application.
     *
     * @return \Illuminate\Foundation\Application
     */
    public function createApplication()
    {
        $app = require __DIR__.'/../bootstrap/app.php';

        $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

        // 接続するデータベースをテスト用のものにする。
        \Config::set('database.default', 'test');

        return $app;
    }

    // テストメソッドが起動される度に実行
    public function setUp()
    {
        parent::setUp();

        // テスト用のデータベースをリセット。毎回リセットしてたら時間かかるから基本使わなかった。
        if ($this->resetDatabase) {
            Artisan::call('migrate:refresh');
            Artisan::call('db:seed');
        }

                // これをして、処理が終わったあとに、rollBackをすることで、DBへの操作をリセットできる。
        \DB::beginTransaction();
    }

        // テストメソッドが終わる度に実行
    public function tearDown()
    {
        \DB::rollBack();
                // Mockery使うので、毎回closeしとく。
        \Mockery::close();
        parent::tearDown();
    }

        // 自作してみたDefinitionファザードをモック。
    // こうすると、Definition::get()と呼び出した時の返り値を好きに決めれる。
    function mockDefDefinition()
    {
        \Definition::shouldReceive('get')
                   ->andReturn[定義ファイル]);
    }

####Ajaxテスト作成
Ajax使ったログイン処理がうまくできるかをチェックする。
#####実行方法
phpunit tests/AjaxTest.phpをすると実行できる。
phpunit tests/AjaxTest.php --group ngtestをすると、@group testとつけたテストメソッドのみ起動するとかできる。

test/AjaxTest.php
class AjaxTest extends TestCase
{
    public function testLoginAjaxOK()
    {
                // POSTのリクエスト
        $request = ['id' => 1, 'pass' => 'test'];
                // Cookieも一緒に送れる
        $cookie = [];
                // Ajaxと認識してもらうために、ヘッダー情報を追加
        $server = ['HTTP_X-Requested-With' => 'XMLHttpRequest'];

        // POSTで、api/login という urlに、アクセスするって意味。
        $response = $this->call('POST', '/api/login', $request, $cookie, [], $server);
                //  Login成功したと仮定して、Response Code 200が返るかチェック
        $this->assertEquals(200, $response->status());
                // Login成功したらLoginCookieというCookieがつくので、つくかチェック
        $this->assertEquals('LoginCookie', $response->headers->getCookies()[0]->getName());
    }

        // メモなので、annotationも記載。これをすると、実行するテストメソッドを選べる。
        /**
      * @group test
     */
    public function testLoginAjaxNG()
    {
                // POSTのリクエスト
        $request = ['id' => 1, 'pass' => 'none'];
                // Cookieも一緒に送れる
        $cookie = [];
                // Ajaxと認識してもらうために、ヘッダー情報を追加
        $server = ['HTTP_X-Requested-With' => 'XMLHttpRequest'];

        // POSTで、api/login という urlに、アクセスするって意味。
        $response = $this->call('POST', '/api/login', $request, $cookie, [], $server);
                //  Login失敗したと仮定して、Response Code 401が返るかチェック
        $this->assertEquals(401, $response->status());
                // Login失敗したらCookie配列がないことをチェック
        $this->assertEquals(true, empty($response->headers->getCookies()));
    }
}

####Modelテストを作成
モックして、引数にいれたり、overloadして、無理やり呼ばれた時の挙動を変えたりしてます。
パーシャルモックも便利!

tests/Models/UserTest.php
<?php
namespace Test\Models;

use App\User;
use App\SuperUser;
use App\ExtraUser;

class UserTest extends TestCase
{
    public function testCountMaleUser()
    {
        // $user = new User();ってしてもいいけど、mockを使う。
        $mock = \Mockery::mock('App\User');
        // こうすると、$mock->countUser()とすると、5が返ってくる。
        $mock->shouldReceive('countUser')->andReturn(5);
        // passthruとすると、元のメソッドがそのまま呼ばれる。
        $mock->shouldReceive('useCountUser')->passthru();

        $this->assertEquals(5, $mock->countUser());
        //  countUserがuseCountUserで呼ばれた場合、mockしたcountUserが呼ばれる。
             $this->assertEquals(5, $mock->useCountUser());

        // 注)mockしてないメソッドを呼び出したらエラーが起こる。
                // 防ぐためには、mockインスタンスに、makePartial()する。(パーシャルモック)
        $mock = \Mockery::mock('App\User')->makePartial();
        // そのため、下のメソッドは、エラーにならない
        $mock->getUser();
        // $mock->where('id', 1); --ただ、これはエラーが起きる
        // __call とかで呼ばれてるメソッドは、パーシャルモックできないっぽい。
    }

    public function testSuperUser()
    {
         $userMock = \Mockery::mock('App\User');
         // with('female')とすると、メソッドの引数が'female'のときのみ、mockされる。
         $userMock->shouldReceive('countUser')->with('female')->andReturn(2);

         // superUserクラスが、引数にUserクラスを取る場合、mockしてコンストラクターインジェクションができる。
         $superUser = new SuperUser($userMock);
    }

    /**
     * @runInSeparateProcess
     * @preserveGlobalState disabled
     */
    public function testExtraUser()
    {
        $userMock = \Mockery::mock('overload:App\User');
        $userMock->shouldReceive('countUser')->with('male')->andReturn(3);

        // 引数でとらずに内部で、App::make('App\User')してる場合は、overloadする。
        // その場合、annotationで上のやつをつけないとエラー起きる。
                $extraUser = new ExtraUser();
    }
}

##モックとスタブ
モックだけじゃなくて、スタブというものもあるらしい。
下のページを読んで勉強
Test DoubleとPHPUnit

###モック
・呼び出し回数を指定
・引数を指定
・モック化したいクラスで、テスト時実行されるメソッドに引数がちゃんと渡ってるかテスト
→ 他クラスのメソッドを呼び出した時、引数が想定通り渡っているか確認。

###スタブ
・呼び出し回数は何回でも
・引数もなんでも起動する
→ 呼び出した時の返り値がわかっている他クラスのメソッドを利用したい。

###サンプル
UserLogic.phpのテストをしたいけど、まだ必要なUser.phpができていない。

.
├── app
│   ├── Logics
│   │   └── UserLogic.php - テスト対象
│   └── User.php - 必要だけど、まだ出来てないファイル
└── tests
    ├── TestCase.php - 初期のもので試した。
    └── UserLogicTest.php - 今回のテストファイル
App\Logics\UserLogic.php
namespace App\Logics;

use App\User;

class UserLogic
{
    public function __construct(User $user)
    {
        $this->user = $user;
    }

    public function getUserName()
    {
        $name = $this->user->getName();
        return "ユーザー名:".$name;
    }

    public function setUserName($name)
    {
        $setName = $name."様";
        $this->user->setName($setName);
    }
}

このUser.phpはまだ未完成でOK。ファイルがなくてもOK。
テストファイルでは、MemberLogicクラスをnewするとき引数が指定されているので、コンストラクターインジェクションが行われない。

App\User.php
namespace App;

class User
{
//    public function getName()
//    {
//        //return 'testName';
//    }
//
//    public function setName($name)
//    {
//        //$this->name = $name;
//    }
}
tests\UserLogicTest.php
// ここでUser.phpの読み込みは必要ない。
use App\Logics\UserLogic;

class UserLogicTest extends TestCase
{
    public function testGetUserName()
    {
        // mockオブジェクトを生成。
        $mock = \Mockery::mock('App\User');
        // これがスタブだと思う。とある関数が来た時、指定した返り値が戻るように設定するだけ。
        $mock->shouldReceive('getName')->andReturn('testName');

        // $mockオブジェクトをコンストラクターインジェクション
        $logic = new UserLogic($mock);
        $testName = $logic->getUserName();
        // getUserNameが想定通り動いていればTrue
        $this->assertEquals('ユーザー名:testName', $testName);
    }

    public function testSetUserName()
    {
        $testName = 'testName';
        // mockオブジェクトを生成。
        $mock = \Mockery::mock('App\User');
        // これがモックだと思う。setNameが呼ばれた時、想定通りの引数が渡っているかをここでテストできる。
        $mock->shouldReceive('setName')->once()->with($testName."様");

        // $mockオブジェクトをコンストラクターインジェクション
        $logic = new UserLogic($mock);
        // setUserNameが想定通り動いていれば、mockでエラー起きない。
        $logic->SetUserName($testName);
    }
}

こんな感じだと思います。間違えてたら、教えていただけると助かります。

##便利なannotation
第2章 PHPUnit 用のテストの書き方
付録B アノテーション

  1. とあるテストの中で、できた変数等を、次のテストで利用したい。
    @depends testExample
  2. テスト結果が正しいか判断するために、比較する配列やオブジェクトを準備しないといけない時便利。
    @dataProvider provideArrayExaple
  3. 想定されたエラーが起こるかテストしたい
    @expectedException PHP_example_Error
  4. テストをグループ分けして実行したい。
    @group testGroupして、phpunit --group testGroupを実行
    他にあったら順次追加

##Tips
###コントローラーテストで、Eloquentを継承したクラスをモックしたいとき
コントローラーをテストする時、Eloquentを継承したクラス利用時、DBアクセスさせないようにモックしないといけないが、対象メソッドが多くなりがちで大変。partialMockも、getとかwhereメソッドに使えない(__callで呼ばれてる関数だから?)
どうするか?

  1. Laravel/データベースレイヤーとのテスト1
    interface時にアクセスするEloquentクラスをテスト時に$app->bindで自作のスタブクラスに切り替える設計にする。そうすると、DBアクセス行わず一定の返り値が返ってくる。

  2. Laravel4.2のリポジトリパターン
    リポジトリーパターンにする。そうすると、コントローラーのテストで、モックするのはリポジトリーでOKなので、すごく楽。

  3. Laravel5のアーキテクチャから学ぶより良いクラス設計#リポジトリ
    Eloquent側では、Scopeメソッドを作成し、コントローラ側でそれを呼ぶようにする。
    そうすると、MockはScopeメソッドだけでいいし、Scopeに名前をわかりやすくつけることで、意図が理解しやすい。

###データベースを利用したクラスをユニットテストしたいとき
Laravel/データベースレイヤーとのテスト2
このクラスでテストするのは、データベースに正しく値が書き込まれるかどうかではなく、意図したSQLが発行されているかをテスト。上記URLを参考に。
注意)Laravel5.2は、illuminate.query使えないので、SQL確認するときは、下のURLに説明されている書き方を使う。
LARAVEL5 実行されるSQL を出力する

##自分で考えた設計方法メモ
持続可能な開発を目指す ~ ドメイン・ユースケース駆動(クリーンアーキテクチャ) + 単方向に制限した処理 + FRP
を参考にしましたが、全部理解できているとは思えないので、間違えてるかもです。

###目的
・実装の流れを、UIやDBなど他のことを気にせず決めることができるようにする。
そのために、UI側の変更による修正はコントローラー内だけで、データ保存側の変更による修正はモデル内だけで、許容できるような設計にして、基本的にユースケースやサービスは、外部に影響されないようにする。
・プログラムがわからない人にも設計に参加しやすくしてもらう。
【改訂版】初歩のUML:第8回 顧客の要求をユースケースに反映させる
にある、ユースケース図とユースケース記述がそのままの形で書けるようにする。

###どうやって作るか
こんな感じの作り方。ModelとDBはセット、ControllerとUI側はセットと考える。

[HTML(API) ── Controller ─]─ UseCase ─[─ Model ── DB(File)]
                                │
                             Service

最初に下のようにどうやって実装するかの処理の流れをユースケースクラスに書く。
そのあと、使用する関数を一つづつ実装していく。

Class MemberUseCase {
  __construct(MemberModel $member, FormatService $format, ViewHistoryModel $view)
  {
     $this->memberModel = $member;
     $this->formatService = $format;
     $this->viewHistoryModel = $view;
  }

  function getMember($searchAttribute)
  {
    //コントローラーから渡される引数で検索条件を受け取る
        
    //メンバーを検索
    $member = $this->memberModel->search($searchAttribute);

    //メンバーの閲覧履歴を取得(通常はrelationで取ってくるけど、メモのため別にメソッド呼び出し)
    $history = $this->viewHistoryModel->getHistoryList($member);

    //情報を表示用に整形
    $formatMember = $this->formatService->forDetail($member, $history);

    return $formatMember;
  }
}

こんな風にすれば、何が行われているか分かりやすい上に、何を作ればいいかも明らか。

###作成するもの
####ユースケース
上に書いたように、処理の流れを書く。ユースケース図の1単位をクラスとして、ユースケース設計をメソッドとして、定義していくだけ。利用する他クラスはできていなくてOK。ユースケースクラスができてから、他クラスを実装する。

####サービス
整形処理とか計算処理とか、UIやDBなどに依存しない処理を書く。
引数が一緒であれば、返り値は変わらないというように、書いて単体テストがやりやすいようにするのが重要だとおもう。
フレームワークが変わっても利用できるように。

####コントローラー
表示側の形式(JSON、Object)や、入力(POST、GET)の違いを吸収する。
ValidateやRequestの取得、JSONへのEncodeなど行う。
テスト:$this->call();を利用Laravel 5.1 テスト カスタムHTTPリクエスト

####モデル
データ保存による違いを吸収する。(DB用のモデルを作ったり、File用のモデルを作ったりして、どんな保存形式でも対応するようにする。Eloquent使うなら、基本的にScopeを利用すべき。
テスト:Fileの場合は、データが取れているか。DBの場合は、Query生成が正しく行われてるかなど。
Laravel/データベースレイヤーとのテスト2

30
39
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?