見出し画像

laravel (sail)でテストしてまっか? (2) assertSee系のテスト

ファイルがアップロードされていない場合のテスト

想定

この画面

この場合、ファイルがアップロードされていない時の使用は以下の通り

  • Simple Uploaderと表示されている

  • Uploaded Filesが表示されている

  • No files uploaded yet.が表示されている

  • Uploadボタンが表示されている

などが考えられる。まあ、書いてみよう。

最初のテストの作成

% ./vendor/bin/sail artisan make:test Uploader/IndexTest


   INFO  Test [tests/Feature/Uploader/IndexTest.php] created successfully.

このようにmake:testする。すると

<?php

namespace Tests\Feature\Uploader;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class IndexTest extends TestCase
{
    /**
     * A basic feature test example.
     */
    public function test_example(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

このようなファイルが生成されるはずだ。これは何気に前回みたExampleと全く変わらない。

イニシャルビューのテスト

class IndexTest extends TestCase
{
    public function test_display_initial_screen(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

まず、テスト関数はtest…から初まる必要がある。あるいはphpdocにそれっぽいディレクティブを与えるんだけど面倒なのでtestから初めましょう。

実際のところ、testから初まっていればどういう名前でもいい。実際に

class IndexTest extends TestCase
{
    public function testデーター無しで最初に起動した画面(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

これもアリ

実行すると

% ./vendor/bin/sail test

   PASS  Tests\Unit\ExampleTest
  ✓ that true is true

   PASS  Tests\Feature\ExampleTest
  ✓ the application returns a successful response                                      0.26s

   PASS  Tests\Feature\Uploader\IndexTest
  ✓ データー無しで最初に起動した画面                                                                   0.02s

  Tests:    3 passed (3 assertions)
  Duration: 0.36s

などとなる。とはいえそれは流石にやんちゃなのでやめておくとしよう

    public function test_initial_screen_without_data(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }

文字が見えている事を保証するとassertSee

  • Simple Uploaderと表示されている

  • Uploaded Filesが表示されている

  • No files uploaded yet.が表示されている

  • Uploadボタンが表示されている

を保証したいのであった、ではそれをダラダラ書いていこう

    public function test_initial_screen_without_data(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
        $response->assertSee('Simple Uploader');
        $response->assertSee('Uploaded Files');
        $response->assertSee('No files uploaded yet.');
        $response->assertSee('Upload');
    }

ただし、冷静に考えるまでもなく、これは、いろんな意味で問題しかないテストである、わかりますか?

問題の修正

まず、assertSeeでのUIのテストは、文言変更に脆弱なので、本当にその文字が出て欲しいものを確実に保証したい所だけに仕掛けるべきである。

というかそもそもUIのテストが本当に必要なのかどうか今一度考えてみてほしい(そこから?)

と自問自答みたいな話をしちゃったけど、本稿はチュートリアルなのでUIのテストをやってみるというのが今回の課題である

…の前にまだviewを出してなかったのでそれを見ていこう

resources/views/uploaders/index.blade.php 

<x-uploader-layout>
  <div class="max-w-2xl mx-auto py-10 px-6 bg-white rounded-lg shadow-md">
    <h1 class="text-2xl font-semibold text-gray-700 mb-5">
      <a href="{{ route('uploaders.index') }}" class="hover:underline">Simple Uploader</a>
    </h1>
    <x-auth-session-status class="mb-4" :status="session('status')" />

    <form method="POST" action="{{ route('uploaders.store') }}" enctype="multipart/form-data" class="space-y-6">
      @csrf

      <div>
        <x-input-label for="file" :value="__('File')" class="block text-sm font-medium text-gray-700" />
        <div class="mt-1 flex items-center">
          <input type="file" id="file" class="block w-full text-sm text-gray-500
          file:mr-4 file:py-2 file:px-4
          file:rounded-full file:border-0
          file:text-sm file:font-semibold
          file:bg-violet-50 file:text-violet-700
          hover:file:bg-violet-100" name="file" required autofocus />
        </div>
        <x-input-error :messages="$errors->get('file')" class="mt-2" />
      </div>

      <div class="flex items-center justify-end mt-4">
        <x-primary-button>
          {{ __('Upload') }}
        </x-primary-button>
      </div>
    </form>
    <div class="mb-8">
      <h2 class="text-xl font-semibold text-gray-600 mb-3">Uploaded Files</h2>

      <div class="overflow-x-auto">
        <table class="min-w-full leading-normal">
          <thead>
            <tr>
              <th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
                {{ __('File Name') }}
              </th>
              <th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
                {{ __('Size') }}
              </th>
              <th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100"></th>
            </tr>
          </thead>
          <tbody>
            @forelse($uploadedFiles as $file)
              <tr>
                <td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">
                  <div class="flex items-center">
                    <div class="ml-3">
                      <p class="text-gray-900 whitespace-no-wrap">
                      <a href="{{ Storage::url('uploaded_files/'. $file->saved_name) }}" class="text-blue-600 hover:text-blue-900">{{ $file->saved_name }}</a>
                      </p>
                    </div>
                  </div>
                </td>
                <td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">
                  <p class="text-gray-900 whitespace-no-wrap">
                    {{ $file->size }}
                  </p>
                </td>

                <td class="px-5 py-5 border-b border-gray-200 bg-white text-sm text-right">
                  <form method="POST" action="{{ route('uploaders.destroy', $file->id) }}" onsubmit="return confirm('{{ __('Are you sure you want to delete this file?') }}')">
                    @csrf
                    @method('DELETE')
                    <x-danger-button>{{ __('Delete') }}</x-danger-button>
                  </form>
                </td>

              </tr>
            @empty
              <tr>
                <td colspan="3" class="px-5 py-5 border-b border-gray-200 bg-white text-sm">
                  <p class="text-gray-900 whitespace-no-wrap text-center">
                    {{ __('No files uploaded yet.') }}
                  </p>
                </td>
              </tr>
            @endforelse
          </tbody>
        </table>
      </div>
    </div>
  </div>
</x-uploader-layout>

通常この

この部分は今

    <h1 class="text-2xl font-semibold text-gray-700 mb-5">
      <a href="{{ route('uploaders.index') }}" class="hover:underline">Simple Uploader</a>
    </h1>

このようになっているが、本来はconfigの

    'name' => env('APP_NAME', 'Laravel'),

これがセットされている事が理想である。従ってジブンならこのようにテストを書く

話が逸れる余談ではあるが、テストの書き方は一定のガイドラインのようなものはあるかもしれないが、ほとんどの場合プロジェクトに依存するので、「こうすればこうなる」みたいな型にハマったものを作るのは難しい。つまり最終的にはテストを書くのはセンスである。センスというのは才能のことを言ってるわけではなく、単純に経験値であるが、場数を熟すだけでなく、仕掛けたものに対する反省をどれだけやってよいものにしようとしたかという事。つまりテストを書いてない人はそもそもこの段階で始まってすらいない


これはConfigをそのままチェックするのではなく発想を切り替えて

    public function test_initial_screen_without_data(): void
    {
        // ユニークなアプリケーション名を生成
        $uniqueAppName = 'TestApp' . \Str::random();

        config(['app.name' => $uniqueAppName]);
        $response = $this->get('/');
        $response->assertStatus(200);
        $response->assertSee($uniqueAppName);

このように捨てconfig値をセットし、それが出てくるかどうかを検査するのであるが、ただ、これはtrueになる。なぜならlayoutが

    <title>{{ config('app.name', 'Laravel') }}</title>

このようになっているからだ。つまりhtml全体ではtitleにapp.nameが埋めこまれて必ず表れてくるので、assertSeeは必ずtrueになる。

これをもうちょい改良し

    public function test_initial_screen_without_data(): void
    {
        // ユニークなアプリケーション名を生成
        $uniqueAppName = 'TestApp' . \Str::random();

        config(['app.name' => $uniqueAppName]);
        $response = $this->get('/');
        $response->assertStatus(200);
        $response->assertSeeInOrder([$uniqueAppName, $uniqueAppName]);

とすることで上から順順にapp titleが2回出る事を保証している。これで失敗するテストが書ける(あえて失敗させている)

' contains "TestAppWhKgpvAFuZtOQaE3" in specified order..

  at tests/Feature/Uploader/IndexTest.php:26
     2223▕         config(['app.name' => $uniqueAppName]);
     24▕         $response = $this->get('/');
     25▕         $response->assertStatus(200);
  ➜  26▕         $response->assertSeeInOrder([$uniqueAppName, $uniqueAppName]);
     2728// $response->assertSee('Uploaded Files');
     29// $response->assertSee('No files uploaded yet.');
     30// $response->assertSee('Upload');


  Tests:    1 failed, 3 passed (5 assertions)
  Duration: 0.38s

このように失敗から始めて、あとで修正するという手法がある。修正してみよう

resources/views/uploaders/index.blade.php 

    <h1 class="text-2xl font-semibold text-gray-700 mb-5">
      {{--
      <a href="{{ route('uploaders.index') }}" class="hover:underline">Simple Uploader</a>
      --}}
      <a href="{{ route('uploaders.index') }}" class="hover:underline">{{ config('app.name') }}</a>
    </h1>

(実際はコメントアウトすら不要だろう)

% ./vendor/bin/sail test

   PASS  Tests\Unit\ExampleTest
  ✓ that true is true                                                                  0.01s

   PASS  Tests\Feature\ExampleTest
  ✓ the application returns a successful response                                      0.26s

   PASS  Tests\Feature\Uploader\IndexTest
  ✓ application name is not default                                                    0.01s
  ✓ initial screen without data                                                        0.01s

  Tests:    4 passed (5 assertions)
  Duration: 0.38s

このようになる

ただしここで気をつけるのは「app.nameが2回表示されること」しかチェックしていないから、なんか適当な所に出現しているとpassしてしまう。タグの中身とかもう少し厳密にチェックをかけてもいいんだけど、実際にはこれくらいでいいと思う。

テストで完全なものを作るのは限界があるし、本来はここはなるべく労力をかけたくない(というか現実の工数としてそこまで余裕が無い事が多い)というトレードオフであるからコンピューターのテストはある程度通す事を前提として書いて、なんか通らなくなったら調べるくらいのノリでやりたい。最終的には人力のテストは不可欠なのであり、自動化されたテストとは相互に補完する関係であるべきである。

UIの変更テストに囚われすぎるな

最初の方に書いときましたよね。テストを書きはじめのころはうれしくなってこのようなUIのチェックを多数仕掛けがちだが、featureテストは実際のところUIをチェックするのはおまけみたいなもんであるから、ここをガッチリきめるのは実は微妙なのだ。しかし前も書いたように今回は初級ドキュメントなのでそういうのも含めていろいろ書いていくよ。これが必要かどうかは後で読んだ皆さんが判断してください。

ちなみに一番ダメなのは無駄なテストを仕掛けすぎた事によりテストが何か沢山通らなくなったのを「ま、あれは対したテストじゃねえから通んなくてもいいか〜」とかいって放置した状態である。そんなんなら最初から書くだけ時間の無駄だからむしろテストなんて書かない方が生産性が高い。

その他のテスト

  • Simple Uploaderと表示されている → configのapp.nameが正しく表示されている

  • Uploaded Filesが表示されている

  • No files uploaded yet.が表示されている

  • Uploadボタンが表示されている

そして次の「Uploaded Filesが表示されている」であるが

全体的に考えてこれ、英語でわざわざ書いてますやんか。ただ、viewは

<h2 class="text-xl font-semibold text-gray-600 mb-3">Uploaded Files</h2>

となっている。ところがこれは「アップロードされたファイル」になるかもしれない

たとえばlaravel langを入れてみる

% ./vendor/bin/sail composer require laravel-lang/lang

そうすると、言語ファイルが大量に入ってくる

config/app.php で

    'locale' => 'ja',

などして

artisan lang:update

などすると

% ./vendor/bin/sail artisan lang:update

   INFO  Collecting translations...

  LaravelLang\Lang\Plugin ........................................................ 14ms DONE

   INFO  Storing changes...

  en.json ......................................................................... 2ms DONE
  en/auth.php ..................................................................... 1ms DONE
  en/pagination.php ............................................................... 0ms DONE
  en/passwords.php ................................................................ 0ms DONE
  en/validation.php ............................................................... 4ms DONE
  ja.json ......................................................................... 1ms DONE
  ja/auth.php ..................................................................... 0ms DONE
  ja/pagination.php ............................................................... 0ms DONE
  ja/passwords.php ................................................................ 0ms DONE
  ja/validation.php ............................................................... 5ms DONE

と入ってくる。このja.jsonに

    "Uploaded Files": "アップロードされたファイル",

など書いて、viewを

      <h2 class="text-xl font-semibold text-gray-600 mb-3">{{ __('Uploaded Files')}}</h2>

とすると

日本語化された

こうなりますわな。つまり

        $response->assertSee('Uploaded Files');

こういうテストはあっさり失敗するわけだ

' [UTF-8](length: 3517) contains "Uploaded Files" [ASCII](length: 14).

  at tests/Feature/Uploader/IndexTest.php:28
     24▕         $response = $this->get('/');
     25▕         $response->assertStatus(200);
     26▕         $response->assertSeeInOrder([$uniqueAppName, $uniqueAppName]);
     27▕
  ➜  28▕         $response->assertSee('Uploaded Files');
     29// $response->assertSee('No files uploaded yet.');
     30// $response->assertSee('Upload');
     31▕     }
     32▕ }

こういうのに対応したければ

        $response->assertSee(__('Uploaded Files'));

こういう風にしておく、とか、まあいろいろ考える必要がある。

    public function test_initial_screen_without_data(): void
    {
        // ユニークなアプリケーション名を生成
        $uniqueAppName = 'TestApp' . \Str::random();

        config(['app.name' => $uniqueAppName]);
        $response = $this->get('/');
        $response->assertStatus(200);
        $response->assertSeeInOrder([$uniqueAppName, $uniqueAppName]);

        $response->assertSee(__('Uploaded Files'));
        $response->assertSee(__('No files uploaded yet.'));
    }

まあこんな感じで、viewも

                    {{ __('No files uploaded yet.') }}

こうなってるなら

こういう風にしたとて

% ./vendor/bin/sail test

   PASS  Tests\Unit\ExampleTest
  ✓ that true is true

   PASS  Tests\Feature\ExampleTest
  ✓ the application returns a successful response                                      0.28s

   PASS  Tests\Feature\Uploader\IndexTest
  ✓ application name is not default                                                    0.01s
  ✓ initial screen without data                                                        0.02s

  Tests:    4 passed (7 assertions)
  Duration: 0.40s

ちゃんとテストは通るということになる。

次回は

で、さすがにUIにassertSee()しまくってても意味ないのでもうちょっとfeatureテストのコアになる部分を見ていこう。

ちなみにassertSeeはbreezeのテストでは一切書かれていない。繰り返しになるがFeatureテストでは文言の不一致のテストにとらわれすぎると大抵崩壊する。ただ、これもどーしても譲れない場所とかに関してはやった方がいいときもあるから、この辺は正に経験、センスが問われる所であろう。


この記事が気に入ったらサポートをしてみませんか?