ฟอร์มที่ให้ผู้ใช้อัปโหลดไฟล์ เช่น รูปโปรไฟล์ ใบเสร็จ 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();
}
}
use App\Http\Controllers\AvatarController;
Route::post('/avatar', [AvatarController::class, 'store'])->middleware('auth');
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')สลับ diskpublicไปใช้โฟลเดอร์storage/framework/testing/disks/publicและล้างโฟลเดอร์นั้นทุกครั้งที่เรียก ไฟล์จาก test จึงไม่ไปปนกับไฟล์จริงในstorage/app/publicUploadedFile::fake()->image('avatar.jpg', 400, 400)วาดรูป jpg ขนาด 400×400 px ขึ้นมาจริง ๆ ruledimensionsจึงอ่านขนาดรูปได้ ถ้าไม่ใส่ขนาด จะได้รูป 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






