เขียน test อัปโหลดไฟล์ใน Laravel ด้วย Pest

เขียน Pest test ให้ฟอร์มอัปโหลดรูป PDF หรือไฟล์ Excel ได้โดยไม่ต้องมีไฟล์จริง ด้วย UploadedFile::fake() กับ Storage::fake() พร้อม test กรณีไฟล์ไม่ผ่าน validation

เผยแพร่
อัปเดต
— เปลี่ยนตัวอย่างเป็น Pest และเพิ่ม test กรณีไฟล์ไม่ผ่าน validation
เวลาอ่าน
3 นาที
สรุปจาก
Eric L. Barnes · laravel-news.com
แท็ก
#Laravel#Testing
ในโพสต์นี้ 5 หัวข้อ
  1. Controller ที่เราจะเขียน test ให้
  2. เขียน test ให้รูปที่อัปโหลดไปถึง disk
  3. ส่งไฟล์ที่ไม่ผ่าน validation แล้วต้องเจอ error
  4. fake ไฟล์ PDF หรือไฟล์ชนิดอื่นด้วย create()
  5. ใช้ไฟล์ตัวอย่างที่มีข้อมูลจริงด้วย createWithContent()

ฟอร์มที่ให้ผู้ใช้อัปโหลดไฟล์ เช่น รูปโปรไฟล์ ใบเสร็จ PDF หรือไฟล์ Excel สำหรับ import ก็ควรมี test เหมือนฟีเจอร์อื่น Laravel มีตัวช่วยให้สองตัว คือ UploadedFile::fake() สร้างไฟล์ปลอมส่งเข้า request กับ Storage::fake() สลับ disk ไปใช้โฟลเดอร์ชั่วคราวระหว่าง test เราจึงไม่ต้องเตรียมไฟล์จริง และไม่มีไฟล์จาก test ค้างปนกับไฟล์ของผู้ใช้

Controller ที่เราจะเขียน test ให้

สมมติแอปมีหน้าให้ผู้ใช้เปลี่ยนรูปโปรไฟล์ Controller รับไฟล์ avatar เช็คว่าเป็นรูป ขนาดไม่เกิน 2 MB กว้างและสูงไม่เกิน 1000 px แล้วเก็บลง disk public (สมมติว่าตาราง users มีคอลัมน์ avatar_path แล้ว)

<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\File;

class AvatarController extends Controller
{
    public function store(Request $request): RedirectResponse
    {
        $request->validate([
            'avatar' => [
                'required',
                File::image()
                    ->max(2048)
                    ->dimensions(Rule::dimensions()->maxWidth(1000)->maxHeight(1000)),
            ],
        ]);

        $path = $request->file('avatar')->store('avatars', 'public');

        $request->user()->update(['avatar_path' => $path]);

        return back();
    }
}

File::image() เป็น rule แบบเขียนต่อกันเป็นสาย max() รับขนาดเป็น KB ถ้าจะบอกเพดานนี้ให้ผู้ใช้อ่านเป็น MB ใช้ Number::fileSize() แปลงให้ได้ อ่าน rule ตัวอื่นของไฟล์ได้ใน Validating Files

เขียน test ให้รูปที่อัปโหลดไปถึง disk

สร้างไฟล์ test ก่อน

php artisan make:test AvatarUploadTest --pest
<?php

use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

it('stores the uploaded avatar', function () {
    Storage::fake('public');

    $user = User::factory()->create();
    $file = UploadedFile::fake()->image('avatar.jpg', 400, 400);

    $this->actingAs($user)
        ->post('/avatar', ['avatar' => $file])
        ->assertRedirect()
        ->assertSessionHasNoErrors();

    Storage::disk('public')->assertExists($file->hashName('avatars'));

    expect($user->fresh()->avatar_path)->toBe($file->hashName('avatars'));
});

บรรทัดที่ควรรู้ในนี้

  • Storage::fake('public') สลับ disk public ไปใช้โฟลเดอร์ storage/framework/testing/disks/public และล้างโฟลเดอร์นั้นทุกครั้งที่เรียก ไฟล์จาก test จึงไม่ไปปนกับไฟล์จริงใน storage/app/public
  • UploadedFile::fake()->image('avatar.jpg', 400, 400) วาดรูป jpg ขนาด 400×400 px ขึ้นมาจริง ๆ rule dimensions จึงอ่านขนาดรูปได้ ถ้าไม่ใส่ขนาด จะได้รูป 10×10 px
  • $file->hashName('avatars') ได้ path เดียวกับที่ store() ตั้งให้ เพราะ Laravel สุ่มชื่อไฟล์ครั้งเดียวแล้วจำไว้ในตัวไฟล์ เราจึงรู้ว่าต้องเช็ค path ไหน
  • assertSessionHasNoErrors() ยืนยันว่า validation ผ่าน ถ้าไม่ผ่าน test จะบอกเลยว่า field ไหนมี error

ถ้าโปรเจกต์ยังใช้ PHPUnit โค้ดในฟังก์ชันใช้ได้เหมือนเดิม แค่ย้ายไปไว้ใน method test_... ของคลาสที่ extends Tests\TestCase

ส่งไฟล์ที่ไม่ผ่าน validation แล้วต้องเจอ error

test ที่สำคัญไม่แพ้กันคือไฟล์ที่ระบบต้องปฏิเสธ ใช้ dataset ของ Pest รัน test เดียวกันกับไฟล์หลายแบบ

it('rejects an invalid avatar', function (UploadedFile $file) {
    Storage::fake('public');

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

    $this->actingAs($user)
        ->post('/avatar', ['avatar' => $file])
        ->assertInvalid(['avatar']);

    Storage::disk('public')->assertMissing($file->hashName('avatars'));
})->with([
    'larger than 2 MB' => fn () => UploadedFile::fake()->image('avatar.jpg')->size(3000),
    'not an image' => fn () => UploadedFile::fake()->create('document.pdf', 100),
    'wider than 1000 px' => fn () => UploadedFile::fake()->image('avatar.jpg', 1200, 800),
]);
  • size(3000) สั่งให้ไฟล์รายงานขนาด 3000 KB เกิน 2048 KB ที่ rule ตั้งไว้ ทั้งที่เนื้อไฟล์จริงเล็กนิดเดียว
  • ไฟล์แต่ละแบบอยู่ใน closure (fn () => …) Pest จะสร้างไฟล์ตอน test รันแต่ละรอบ
  • assertInvalid(['avatar']) เช็คว่า avatar มี error ใช้ได้ทั้งฟอร์มปกติที่ error อยู่ใน session และ request แบบ JSON ที่ได้ 422 กลับมา ถ้าเป็นฟอร์มปกติ assertSessionHasErrors('avatar') ก็ได้ผลเหมือนกัน
  • assertMissing() ยืนยันว่าไฟล์ที่ไม่ผ่านไม่ได้ลง disk

ใครยังไม่คุ้นกับ validation ใน Laravel อ่าน Laravel Validation ทำได้กี่แบบ ก่อนได้

fake ไฟล์ PDF หรือไฟล์ชนิดอื่นด้วย create()

ไฟล์ที่ไม่ใช่รูป ใช้ create() ใส่ชื่อไฟล์กับขนาดเป็น KB

// ไฟล์ PDF ขนาด 500 KB
UploadedFile::fake()->create('document.pdf', 500);

// กำหนด MIME type เองใน argument ที่สาม
UploadedFile::fake()->create('document.pdf', 500, 'text/plain');

ปกติ Laravel เดา MIME type จากนามสกุลไฟล์ให้อยู่แล้ว ส่วน argument ที่สามใช้ตอนอยากกำหนดเอง เช่น test ว่า rule mimetypes:application/pdf ปฏิเสธไฟล์ที่ชื่อลงท้าย .pdf แต่ MIME type เป็นอย่างอื่น

ใช้ไฟล์ตัวอย่างที่มีข้อมูลจริงด้วย createWithContent()

ฟีเจอร์ import ต้องอ่านข้อมูลในไฟล์จริง ๆ เราเตรียมไฟล์ตัวอย่างไว้ใน tests/Fixtures/products.xlsx (สมมติว่ามีสินค้า 3 แถว) แล้วส่งเนื้อไฟล์เข้า createWithContent()

<?php

use App\Models\Product;
use App\Models\User;
use Illuminate\Http\UploadedFile;

it('imports products from an Excel file', function () {
    $user = User::factory()->create();
    $file = UploadedFile::fake()->createWithContent(
        'products.xlsx',
        file_get_contents(base_path('tests/Fixtures/products.xlsx')),
    );

    $this->actingAs($user)
        ->post('/products/import', ['file' => $file])
        ->assertRedirect();

    expect(Product::count())->toBe(3);
});

ไฟล์ที่ได้มีเนื้อหาและขนาดเท่าไฟล์ตัวอย่าง ส่วนชื่อไฟล์เราตั้งใหม่ได้ ใช้ได้กับ CSV, .txt หรือไฟล์ชนิดไหนก็ได้ที่โค้ดต้องอ่าน

ต้นเรื่องคือ Testing File Uploads with Laravel ของ Laravel News ส่วนตัวช่วยที่เหลือดูได้ใน HTTP Tests และ Filesystem Testing ของ Laravel

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →