[Laravel]テスト実行時のエラーまとめ

##はじめに
この記事はプログラミング初学者による備忘録用の記事であり、また、少しでも他の初学者のお役に立てればと思い書いています。

今回は、PHPUnitを使用したテスト実行時に発生したエラーについての解決策を箇条書きで記録しておきたいと思います。

※新しいエラーが発生した場合、その都度追記していきます。

間違いなどがございましたら、ご指摘のほどよろしくお願い致します。

##General error: 1 Cannot add a NOT NULL column with default value NULL
``エラー例``

```php
Schema::table('example', function (Blueprint $table){
    $table->string('example');
});

Schema::table('example', function (Blueprint $table){
    $table->string('example2');
});

```
SQLiteの仕様では、テーブルを最初から追加するときには、NOT NULLを指定することができるが、カラムを追加するときはNOT NULLが指定できないらしい。

``解決策``
NULL可能なフィールドとして追加し、後に直接NULL不可能なフィールドに変更することで解決できる。

```php
Schema::table('example', function (Blueprint $table){
    $table->string('example')->nullable();
});

Schema::table('example', function (Blueprint $table){
    $table->string('example2')->nullable(false)->change();
});
```

``参考:``[laracasts [migrations] SQLite General error: 1 Cannot add a NOT NULL column with default value NULL](https://laracasts.com/discuss/channels/general-discussion/migrations-sqlite-general-error-1-cannot-add-a-not-null-column-with-default-value-null)

##Expected status code 200 but received 302.Failed asserting that false is true.


``エラー例``

```php
public function testExample() 
    {    
        $response = $this->get(route('articles.index'));
        //ログインが必要な処理の際にログインした状態を作成していない
        $response->assertStatus(200)
            ->assertViewIs('articles.index');
    }
```
ログイン中でない状態でアクセスした場合、リダイレクトを示す302のステータスコードが返ってくる。
``参考:``[302 Found MDN](https://developer.mozilla.org/ja/docs/Web/HTTP/Status/302)

``解決策``
DBにユーザーデータを作成し、ログインした状態を作り出してテストを実行する。

```php
public function testExample() 
    {
        $user = factory(User::class)->create();

        $response = $this->actingAs($user)
            ->get(route('articles.index'));

        $response->assertStatus(200)
            ->assertViewIs('articles.index');
    }
```


``補足``
**actingAs:** 引数として渡したUserモデルにてログインした状態を作り出します。
その上で、get(route('articles.index'))を行うことで、ログイン済みの状態で一覧画面へアクセスしたことになり、そのレスポンスは変数$responseに代入されます。

**assertStatus:** クライアントのレスポンスが指定したコードであることを宣言。引数には、HTTPレスポンスのステータスコードを渡します。

例えば正常レスポンスを示す200を渡すと、
・ 200であればテストに合格
・ 200以外であればテストに不合格
となります。

**assertViewIs:** ルートにより、指定したビューが返されたことをアサート。


##Failed asserting that...
``エラー例``

```php
public function testLoginIndex()
    {
        $response = $this->get(route('login'));

        $response->assertStatus(200)
            ->assertViewIs('auth.login')
            ->assertSee('ユーザー登録はこちら')
            ->assertSee('ログイン')
            ->assertSee('かんたんログイン');//正しくはゲストログイン
    }
```
テスト実行時に、assertViewIsメソッドで指定したレスポンス(view)上にないデータ(文字列)をassertSeeメソッドで指定した場合に発生する。

```
//略
</body>\n
\n
</html>' contains "かんたんログイン".

//このように指定した"かんたんログイン"という文字列はview上にない為、<!DOCTYPE html></html>からはじかれて表示される。
```

``解決策``
**assertSee:** 指定したルートで返ってくるレスポンス(view)に対して、自分がテストしたい文字列を指定する。

```php
public function testLoginIndex()
    {
        $response = $this->get(route('login'));

        $response->assertStatus(200)
            ->assertViewIs('auth.login')
            ->assertSee('ユーザー登録はこちら')
            ->assertSee('ログイン')
            ->assertSee('ゲストログイン');//正解
    }
```


``補足``
**assertViewIs:** ルートにより、指定したビューが返されたことをアサート。
**assertSee:** 指定した文字列がレスポンスに含まれていることをアサート。


##Response status code [404] is not a redirect status code.
``エラー例``

```php
public function testGuestStore()
    {
        $response = $this->get(route('articles.store'));
        //route('articles.store')はルーティングでpostメソッドを使用
        $response->assertRedirect('login');
    }
```
laravelのルーティングで設定したメソッドと異なるメソッドを指定した場合に発生する。

``解決策``
必ずルーティングと同じメソッドを使用する。

```php
public function testGuestStore()
    {
        $response = $this->post(route('articles.store'));
        //ルーティングと同じpostメソッドを使用する
        $response->assertRedirect('login');
    }
```

``補足``
**assertRedirect:** 引数として渡したURLにリダイレクトされたかどうかをテストします。

##Failed asserting that a row in the table[xxx]

``エラー例``

```php
public function testAuthStore()
    {
        // テストデータをDBに保存
        $user = factory(User::class)->create();

        $title = "テストタイトル";
        $user_id = $user->id;
        //$body = "テスト本文";記述漏れ

        $response = $this->actingAs($user)
            ->post(route(
                'articles.store',
                [
                    'title' => $title,
                    'user_id' => $user_id,
                    //'body' => $body,記述漏れ
                ]
            ));
      //略
}
```
 テスト実行時、NOT NULL制約があるカラムに対して値を代入する処理を忘れていた場合に発生する。

``解決策``
対象となるテーブルのNOT NULL制約付きカラムは必ず設定する。

```php
public function testAuthStore()
    {
        // テストデータをDBに保存
        $user = factory(User::class)->create();

        //NOT NULL制約のカラムは必ず設定する
        $title = "テストタイトル";
        $user_id = $user->id;
        $body = "テスト本文";

        $response = $this->actingAs($user)
            ->post(route(
                'articles.store',
                [
                   'title' => $title,
                   'user_id' => $user_id,
                   'body' => $body,//NOT NULL制約のカラムは必ず設定する
                ]
            ));
      //略
}
```

##ErrorException: Trying to get property '○○' of non-object

``エラー例``

```php
public function testAuthEdit()
    {
        $this->withoutExceptionHandling();

        $article = factory(Article::class)->create()->each(function (Article $article) {
        $article->newsLink()->save(factory(NewsLink::class)->make());
        });
//1つしか作ってない$articleに対してeach()を使用してリレーション先のデータも作成
//->エラーの原因

        $user = $article->user;
//ErrorException: Trying to get property 'user' of non-objectとなる
         
//略
    }
```
factoryを使用したテスト用データをきちんと作成できていない場合に発生しやすい。
例えば、上記のように1つしか作ってないarticleデータに対してリレーション先のデータをeach()で作成すると、$articleが違う形式に変換されエラーが発生する。

``解決策``
1つのデータのみ作成の場合、eachを使用せず処理を分けて実行するように設定する。

```php
public function testAuthEdit()
    {
        $this->withoutExceptionHandling();

        $article = factory(Article::class)->create();
        $article->newsLink()->save(factory(NewsLink::class)->make());

        $user = $article->user;
        dd($user); //dd()で確認、データ作成に成功
//略
    }
```

``補足``
**生成したモデルにリレーションを付けることが可能**
複数モデルの生成にcreateメソッドを使用する場合、インスタンスのコレクションが返されます。そのため、コレクションで使用できるeachなどの便利な関数が利用できます。


##AuthorizationException: This action is unauthorized.

``エラー例``

```php
public function testAuthEdit()
    {
        $this->withoutExceptionHandling();

        $article = factory(Article::class)->create();
        $article->newsLink()->save(factory(NewsLink::class)->make());

        $user = $article->user;

        $response = $this->actingAs($user)->get(route('articles.edit', ['article' => $article]));
        // actingAs()を使った認証の際にエラーが発生する

        $response->assertStatus(200)->assertViewIs('articles.edit');
    }
```
Laravel Policyを使った認証を参照しています。実装したテストとPolicyの間で不具合があった場合にエラーが発生、authorizeメソッドは、ユーザーが認証されていない場合、例外を投げます。

``解決策``
ArticlePolicy.phpの``return $user->id === $article->user_id;``を修正する。

```php 
// 修正前
public function update(User $user, Article $article)
    {
        return $user->id === $article->user_id;
    }
public function delete(User $user, Article $article)
    {
        return $user->id === $article->user_id;
    }
```

```php
// 修正後
public function update(User $user, Article $article)
    {
        return $user->id === $article->user->id;
    }
public function delete(User $user, Article $article)
    {
        return $user->id === $article->user->id;
    }
```

##Failed asserting that two strings are equal.
``エラー例``

```php
public function testCreate()
    {
        $response = $this->post(route('register'),
                [
                    'name' => 'testUser',
                    'email' => 'test@example.com',
                    'password' => 'password123',
                    'password_confirmation' => 'password123',
                ]
            );

        $response->assertRedirect(route('articles.index'));
    }
```

```
There was 1 failure:
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'https://localhost/articles'
+'https://localhost'
```
POST後に指定したページにリダイレクト可能かを確かめる為に、リダイレクトテストを実行する際は、back()で前の画面に戻るが、テストの仕様上、前の画面は保存されていない。
従って、``https://localhost``が返されてしまう。

``解決策``
``->from()``を使用して遷移先の画面を表示する(リダイレクトテスト)際に使用する、urlを指定する。
今回の場合だと、エラーメッセージ``-'https://localhost/articles'``
``+'https://localhost'``と表示されているので、**->from('articles')**とする。

```php
    public function testCreate()
    {
     $response = $this->from('articles')->post(route('register'),
                [
                    'name' => 'testUser',
                    'email' => 'test@example.com',
                    'password' => 'password123',
                    'password_confirmation' => 'password123',
                ]
            );

     $response->assertRedirect(route('articles.index'));
    }
```

##A facade root has not been set.
``エラー例``

```php
<?php

namespace Tests\Unit\Requests;

use Illuminate\Support\Facades\Validator;
use App\Http\Requests\Article\StoreRequest;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use PHPUnit\Framework\TestCase;// ここを修正する

class ArticleStoreRequestTest extends TestCase
{
    use RefreshDatabase;
//略
```
``PHPUnit\Framework\TestCase``はPHPUnit のクラスです。従って、これをuseしてもファサードを動かすことができずエラーが発生します。

``解決策``
Laravel のクラスである``Tests\TestCase``をuseする。

```php
<?php

namespace Tests\Unit\Requests;

use Illuminate\Support\Facades\Validator;
use App\Http\Requests\Article\StoreRequest;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;// Tests\TestCaseに変更する

class ArticleStoreRequestTest extends TestCase
{
    use RefreshDatabase;
//略
```

##参考文献
[Lravel6.x HTTPテスト(公式ドキュメント)](https://readouble.com/laravel/6.x/ja/http-tests.html)
[Lravel6.x Laravel Dusk(公式ドキュメント)](https://readouble.com/laravel/6.x/ja/dusk.html)
[Lravel6.x Laravel DBのテスト(公式ドキュメント)]
(https://readouble.com/laravel/6.x/ja/database-testing.html)