laravelでPHPUnitを使ってテストをする

# 前書き
今回は作成中のポートフォリオの処理にミスがないか、確認するためのテストの作成を行いました。

# 環境

https://qiita.com/ucan-lab/items/56c9dc3cf2e6762672f4

上記記事に準じて環境構築を行っています、

# 実現したいこと
・各コントローラーの処理のテストをしたい。
→リソースコントローラにあたる7つのメソッド(index,edit,create,show,store,delete,update)と画像登録のテスト

# 対応方法
①事前設定

・新しくテストを作成します
```
php artisan make:test テスト名
```

--unitコマンドをつけると、tests/Unitディレクトリに、
つけないとtests/Featureディレクトリにテストが作成されます。

Featureテストは、小さい単位(今回は各コントローラのメソッド)が対象になります。
Unitテストは、アプリケーションを利用しないため、Eloquentのメソッドが使用できない等の制約がありますが、テストの実行が軽いです。

モデル名を使ってテストを作成すると自動で紐付けされましたが、そうでない場合は、テスト内にuse モデル名を記載し連携させる必要があります。

今回はFeatureテストを作成していきます。

②テスト用DBの作成
今回はdockerで環境構築しているので、テスト用DBコンテナの作成を実施します。下記記事を参照させていただきました。とても分かり易かったです、

https://qiita.com/db_container/items/99583772693051ab2465

```docker-compose.yml
 db-test:
    build: ./infra/mysql-test
    volumes:
      - db-store:/db-test/var/lib/mysql
    ports:
      - 3000:3306
```
※インデントがずれるとエラーが出るので注意

```dockerfile:infra/mysql-test/dockerfile
FROM mysql/mysql-server:8.0

ENV MYSQL_DATABASE=laravel_local_test \
  MYSQL_USER=phper \
  MYSQL_PASSWORD=secret \
  MYSQL_ROOT_PASSWORD=secret \
  TZ=Asia/Tokyo
```


元記事では設定ファイル(.my.cnf)が入っていましたが、今回は省略しています。

.env.testingファイル(テスト用の環境変数を記載)を作成します。
phpunit.xmlの内容に合うよう、既存の.env.exampleファイルをコピーして編集します。

```.env.testing
APP_ENV=testing
APP_KEY=

DB_CONNECTION=mysql
DB_HOST=db-test
DB_PORT=3306
DB_DATABASE=laravel_local_test
DB_USERNAME=phper
DB_PASSWORD=secret
```

```
php artisan key:generate --env=testing
```

テスト用DBをmigrateします。
```
docker compose exec app php artisan migrate --env=testing
```
--env=testingコマンドをつけることで、.env.testingの環境変数を参照してDBを作成します。

③テストコードの作成
今回は例としてArticleControllerを対象にします。
Factoryの記載は割愛します。

```php:create_articles_table
  public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
            $table->foreignId('radio_id')->constrained('radios')->onDelete('cascade');
            $table->date('radio_date');
            $table->text('body');
            $table->text('link')->nullable();
            $table->timestamps();
        });
    }
```

```php:tests/Feature/ArticleTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\Models\Article;
use App\Models\User;

class ArticleTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */

    use RefreshDatabase;

    public function setUp(): void
    {
        parent::setUp();

        $this->article = Article::factory()->create();

        $this->user = User::factory()->create();

        $this->data = ([
            'user_id' => $this->article->user_id,
            'radio_id' => $this->article->radio_id,
            'radio_date' => $this->article->radio_date,
            'body' => $this->article->body,
            'link' => $this->article->link,
        ]);
    }

    //index
    public function test_index()
    {
        $response = $this->actingAs($this->user)
            ->get('/articles');
        $response->assertStatus(200);
    }

    //create
    public function test_create()
    {
        $response = $this->actingAs($this->user)
            ->get('/articles/create');
        $response->assertStatus(200)
            ->assertViewHas('radios');
    }

    //store
    public function test_store()
    {
        $response = $this->actingAs($this->user)
            ->post('/articles', $this->data);
        $response->assertRedirect('/articles');
        $this->assertDatabaseHas('articles', [
            'user_id' => $this->article->user_id,
            'radio_id' => $this->article->radio_id,
            'radio_date' => $this->article->radio_date,
            'body' => $this->article->body,
            'link' => $this->article->link,
        ]);
    }
    
    //edit
    public function test_edit()
    {
        $response = $this->actingAs($this->article->user)
            ->from('/articles/' . $this->article->id)
            ->get('/articles/' . $this->article->id . '/edit');
        $response->assertStatus(200)
            ->assertViewHas('radios');
    }

    //update
    public function test_update()
    {
        $response = $this->actingAs($this->article->user)
            ->from('/articles/' . $this->article->id . '/edit')
            ->put(
                '/articles/' . $this->article->id,
                [
                    'user_id' => $this->article->user_id,
                    'radio_id' => $this->article->radio_id,
                    'radio_date' => $this->article->radio_date,
                    'body' => 'aaa',

                ]
            );
        $response->assertRedirect('/articles');
        $this->assertDatabaseHas('articles', [
            'body' => 'aaa',
        ]);
    }

    //destroy
    public function test_destroy()
    {
        $response = $this->actingAs($this->article->user)
            ->delete('/articles/' . $this->article->id);
        $response->assertRedirect('/articles');
        $this->assertDatabaseMissing('articles', $this->data)
            ->assertEquals(0, Article::count());
    }
}
```

>use RefreshDatabase;

テストを実行する度に、migrate:refreshされます。


>$this->actingAs(___)

actingAs内に取る引数は、

登録済みユーザーは、$this->user、

投稿者のみ権限がある場合は、$this->article->user 

## 画像投稿

```php:ArticleTest
    public function test_image_store()
    {
        $image = HttpUploadedFile::fake()
            ->image('hoge.png');

        $response = $this->actingAs($this->user)
            ->post('/articles', [
                'image' => $image,
            ]);
        $response->assertRedirect('/articles');
        $this->assertDatabaseHas('articles', [
            'image' => 'hoge.png',
        ]);
    }

    public function test_image_update()
    {
        $image = HttpUploadedFile::fake()
            ->image('hogehoge.png');

        $response = $this->actingAs($this->user)
            ->from('/articles/' . $this->article->id)
            ->put('/articles/' . $this->article->id, [
                'image' => $image,
            ]);
        $response->assertRedirect('/articles/' . $this->article->id);
        $this->assertDatabaseHas('articles', [
            'image' => 'hogehoge.png',
        ]);
    }

```


>$image = HttpUploadedFile::fake()

テスト用のダミー画像を作成します。

④テスト実行
```
docker compose exec app php artisan test
```
もしくは
```
./vendor/bin/phpunit
```

# 最後に
より良いテストコードの記述方法や投稿内容に誤りがありましたら、ご指摘いただけると助かります。

今後も学習内容の備忘録として投稿していきたいと思います。

また、自動テスト(CI/CD)についても調べて、今後実装していきたいです。


# 参考

https://readouble.com/laravel/9.x/ja/testing.html

https://qiita.com/adgjmptwgw/items/4fa247e7458048777b77