Laravelでファイルのアップロード・ダウンロードのテスト

Laravelでファイルのアップロード・ダウンロードのテストを試していたら躓いたことが多かったので備忘録です。

環境

  • PHP: 7.4.7
  • Laravel: 7.15.0

事前準備

ControllerやModel、ルーティングは以下のとおりです。
また、ファイル保存先の設定としてはconfig/filesystems.phpで使用するdiskをpublicに設定しているので、 ファイルがアップロードされた時に、storage/app/public以下にあるtest ディレクトリ以下にファイルを保存します。

ルーティング

<?php

use Illuminate\Support\Facades\Route;

Route::get('/file', 'FileController@index')->name('file.index');
Route::post('/file', 'FileController@upload')->name('file.upload');
Route::get('/file/{fileId}', 'FileController@download')->name('file.download');

一つのページでアップロードとダウンロードが出来る機能を有しているものを想定しています。

Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\UploadedFile;

class File extends Model
{
    protected $fillable = [
        'name',
        'path'
    ];

    public function saveFile(UploadedFile $file)
    {
        $name = $file->getClientOriginalName();
        $path = $file->store('test');

        return $this->fill(compact('name', 'path'))->save();
    }
}

Filesテーブルにファイル名とパスを保存します。

File Storage - Laravel - The PHP Framework For Web Artisans

Controller

<?php

namespace App\Http\Controllers;

use App\Models\File;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class FileController extends Controller
{
    public function index()
    {
        $files = File::all();
        return view('file.index', compact('files'));
    }

    public function upload(Request $request)
    {
        $file = new File();
        $file->saveFile($request->file('upload-file'));

        return redirect(route('file.index'));
    }

    public function download(Int $fileId)
    {
        $file = File::find($fileId);

        return Storage::download($file->path, $file->name);
    }
}

アップロード時はModelに記述した処理で保存し、
ダウンロード時はDBに保存した情報をもとにしてファイルを返します。
PDFや画像をダウンロードせずにブラウザで表示したい場合は以下のように記述します。

        $file = File::find($fileId);
        $headers = [
            'Content-Disposition' => 'inline; filename="' . $file->name . '"'
        ];

        return response()->file($file->path, $headers);

File Storage - Laravel - The PHP Framework For Web Artisans

テスト

<?php

namespace Tests\Feature;

use App\Models\File;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

class FileControllerTest extends TestCase
{
    use RefreshDatabase;

    public function testFileUpload()
    {
        Storage::fake('public');
        $file = UploadedFile::fake()->image('temp.jpg');

        $this->post(route('file.upload'), [
            'upload-file' => $file
        ]);
        
        Storage::disk('public')->assertExists('test', $file->hashName());
    }

    public function testFileDownload()
    {
        Storage::fake('public');
        $file = UploadedFile::fake()->image('temp.jpg');
        $this->post(route('file.upload'), [
            'upload-file' => $file
        ]);

        $uploadedFile = File::first();
        $response = $this->get(route('file.download', ['fileId' => $uploadedFile->id]));
        
        $this->assertTrue($response->headers->get('content-disposition') == 'attachment; filename=' . $uploadedFile->name);
    }
}

アップロード

テスト用のdiskとしてpublic を、テスト終了後削除されるデータとしてtemp.jpg を作成します。
ファイルのpostを実行した後、public以下のtest(保存先にModelで指定したdisk)にtemp.jpgが存在することを確認します。
ファイル名はstoreAs()で指定しない限り一意なものになっています。

ダウンロード

アップロードと同様に、一度ファイルをアップロードして、
ダウンロードされたファイル名が保存したファイルであることを確認しています。

実行

ひとまずテストを実行してみると問題なく通ることが確認出来ます。

# php artisan test

   PASS  Tests\Feature\FileControllerTest
  ✓ file upload
  ✓ file download

  Tests:  2 passed
  Time:   2.62s

CircleCI

CircleCIのconfig.ymlも記載します。apt-getでインストールしているものは最低限のものです。
また、CircleCIで用いる設定ではDB_HOST127.0.0.1にしておくことは忘れがちなので注意です。

version: 2
jobs:
  build:
    docker:
      - image: circleci/php:7.4-apache
      - image: postgres:alpine
        environment:
          POSTGRES_DB: test
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
    environment:
      - APP_DUBUG: true
      - APP_ENV: testing
    working_directory: ~/laravel
    steps:
      - checkout
      - run:
          name: Update apt-get
          command: sudo apt-get update
      - run: sudo apt-get install -y libpq-dev libjpeg-dev libpng-dev
      - run: sudo docker-php-ext-configure gd --with-jpeg
      - run: sudo docker-php-ext-install pdo pdo_pgsql gd
      - run:
          name: Setup Laravel testing environment variables for CircleCI test
          command: cp .env.testing .env
      - restore_cache:
          keys:
            - composer-v1-{{ checksum "composer.json" }}
            - composer-v1-
      - run: composer install -n --ignore-platform-reqs
      - save_cache:
          key: composer-v1-{{ checksum "composer.json" }}
          paths:
            - vendor
     - run:
          name: Run Phpunit
          command: php artisan test

まとめ

  • Storage::fake()のあたりが公式ページ通りに書いても上手く行かず、だいぶ手間取ってしまっていた
  • filesystems.phpでどこに保存先を指定しているのかをちゃんと理解しておく必要がある

失敗談

  • アップロードしたファイルのパスを間違ってstorage/以下ではなく、public/以下にしてしまったことでローカルではテストが成功するのにCircleCIでは失敗するという事態になった
  • php artisan storage:linkをCircleCIで実行することで解決はしたが、そもそもファイルの保存先を理解できていなかったのが問題
  • storage/framework/testing以下にテスト時のファイルが作成されるので、それを見ながら動作を確認することでデバッグしていた

参考サイト