🌏

Laravel9でS3のファイル読み込みテストを書く

2022/05/31に公開

背景

LaravelでS3とのやり取りの処理を書く機会がありました.
テストの書き方について忘れてしまいそうだったので,メモとして残しておこうと思いまとめてみました.

環境

OS : MacOS(intel)
Laravel version : 9.14.1
PHP version : 8.1.6

サンプル

https://github.com/katsuya-n/pub_laravel_sail_s3

Laravel Sailでプロジェクトを作成・起動する

Laravelを簡単に動かせるLaravel Sailのプロジェクトを作成します.

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

$ curl -s "https://laravel.build/example-app?with=mysql,redis" | bash

1回パスワードを求められるので入力して完了です.

dockerコンテナを立ち上げていきます

$ cd example-app/
$ ./vendor/bin/sail up -d

http://localhost/ を開いてデフォルトページが表示されていれば,正しく起動できています.

# アプリケーションコンテナに入る
$ ./vendor/bin/sail bash
Xdebug: [Config] Invalid mode 'false' set for 'XDEBUG_MODE' environment variable, fall back to 'xdebug.mode' configuration setting (See: https://xdebug.org/docs/errors#CFG-C-ENVMODE)
PHP 8.1.6 (cli) (built: May 17 2022 16:46:54) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.1.6, Copyright (c) Zend Technologies
    with Zend OPcache v8.1.6, Copyright (c), by Zend Technologies
    with Xdebug v3.1.4, Copyright (c) 2002-2022, by Derick Rethans
 
# Laravelのバージョンを確認
$ php artisan --version
Xdebug: [Config] Invalid mode 'false' set for 'XDEBUG_MODE' environment variable, fall back to 'xdebug.mode' configuration setting (See: https://xdebug.org/docs/errors#CFG-C-ENVMODE)
Laravel Framework 9.14.1

$ exit

S3の準備

今回はテストを書くのが目的ですが,通常の動作確認もしておきたいと思います.
まず,S3バケットを作成します.

バケットの作成完了後,txtファイルを作成したバケットにアップロードします.中身は何でも良いのですが,今回は以下のようにしました.

s3_sample.txt
今日は暑いですね.
もう5月も終わりです.1年の5/12が過ぎました.今年の残り7ヶ月はどのように過ごしますか?

2022.05.31

flysystem-aws-s3-v3をインストール

LaravelからS3とやり取りするのに必要なライブラリをインストールします.公式にも挙げられているflysystem-aws-s3-v3を入れていきます.

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

# アプリケーションコンテナに入る
$ ./vendor/bin/sail bash

$ composer require league/flysystem-aws-s3-v3 "^3.0"

$ exit

S3にアップロードしたtxtの中身を表示してみる

S3にアップロードした中身を取得して,viewで表示するだけの単純な処理を書いていきます.

example-app/routes/web.php
...
// 追記
use App\Http\Controllers\S3TxtController;
...
// 追記
Route::get('/s3/txt', [S3TxtController::class, 'index']);
...
example-app/app/Http/Controllers/S3TxtController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Storage;

class S3TxtController extends Controller
{
    public function index()
    {
        $disk = Storage::disk('s3');
        $data = $disk->get('s3_sample.txt');

        if ($data === null) {
            throw new \Exception('error');
        }

        return view('s3.index',
            compact('data')
        );
    }
}
example-app/resources/views/s3/index.blade.php
<p>{{$data}}</p>

すっ飛ばしましたが,S3へのアクセス権限を付与したIAMユーザーを作成しておく必要があります.作成したIAMユーザーのアクセスキーとシークレットアクセスキーは.envに設定します.

example-app/.env
...
AWS_ACCESS_KEY_ID=[S3へのアクセス権限を付与したアクセスキー]
AWS_SECRET_ACCESS_KEY=[S3へのアクセス権限を付与したシークレットアクセスキー]
AWS_DEFAULT_REGION=[作成したバケットのリージョン名]
AWS_BUCKET=[作成したバケット名]
...

http://localhost/s3/txt にアクセスするとS3にアップロードしたs3_sample.txtの内容が表示されていることが確認できます.

テストを書く

いよいよ本題のテストを書いていきます.
S3へファイルを取得する部分は,txtの中身を変更したりしたいものです.テストではS3へ行かずモック化のようにしたいのが,今回のキモになります.
どうすれば良いのかというと,Storageファザードのfakeメソッドを用いればOKです.

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

use Illuminate\Support\Facades\Storage;
...
    Storage::fake('s3');

のように書くことで,テストで$disk->get('s3_sample.txt');したときにS3へファイルを取りに行くのではなく,ローカルのexample-app/storage/framework/testing/disks/s3/s3_sample.txtを取得しに行くようにすることができます.
Storage.phpの$root = storage_path('framework/testing/disks/'.$disk);で設定しているようです.なのでモック化しているわけではなく,取得先がローカルのテスト用ディレクトリに向く形になります.

example-app/vendor/laravel/framework/src/Illuminate/Support/Facades/Storage.php
...
    public static function fake($disk = null, array $config = [])
    {
        $disk = $disk ?: static::$app['config']->get('filesystems.default');

        $root = storage_path('framework/testing/disks/'.$disk);

        if ($token = ParallelTesting::token()) {
            $root = "{$root}_test_{$token}";
        }

        (new Filesystem)->cleanDirectory($root);

        static::set($disk, $fake = static::createLocalDriver(array_merge($config, [
            'root' => $root,
        ])));

        return tap($fake)->buildTemporaryUrlsUsing(function ($path, $expiration) {
            return URL::to($path.'?expiration='.$expiration->getTimestamp());
        });
    }
...

上記を用いてテストを書いてみます.
Storage::disk('s3')->put('s3_sample.txt', $data);でテスト用のtxtを作成しておいて,$response = $this->get('/s3/txt');でそのファイルを取得しに行っています.

example-app/tests/Feature/S3TxtTest.php
<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Support\Facades\Storage;

class S3TxtTest extends TestCase
{
    /**
     * 正常系.
     */
    public function testGeneral(): void
    {
        $data = 'テスト用Contentsです';
        Storage::fake('s3');
        Storage::disk('s3')->put('s3_sample.txt', $data);

        $response = $this->get('/s3/txt');

        $response->assertStatus(200)
            ->assertSee('Contents')
            ->assertViewIs('s3.index')
            ->assertViewHasAll([
                'data' => $data,
            ]);
    }

    /**
     * 異常系.
     */
    public function testNoFileS3(): void
    {
        Storage::fake('s3');

        $response = $this->get('/s3/txt');

        $response->assertStatus(500);
    }
}

fakeメソッドの場合,テスト用ディレクトリのファイルを全て削除してしまいます.persistentFakeメソッドを用いると消さないようなので,どこのテスト用ディレクトリを使っているかを知りたい時はpersistentFakeメソッドを使ってみると良さそうです.

テストを実行し,成功することを確認して完了です.

$ ./vendor/bin/sail test
株式会社ゆめみ

Discussion