Courses

Testing in Laravel 9 For Beginners: PHPUnit, Pest, TDD

Edit Product: Testing correct values in form inputs

The next obvious thing we would build in such a project is editing the product. The edit form should contain the exact values we will test.


Laravel Code

Before adding the test, we first need an edit form, routes, and controller methods.

app/Http/Controllers/ProductController.php:

use App\Models\Product;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
 
class ProductController extends Controller
{
// ...
 
public function store(StoreProductRequest $request): RedirectResponse
{
Product::create($request->validated());
 
return redirect()->route('products.index');
}
 
public function edit(Product $product): View
{
return view('products.edit', compact('product'));
}
}

routes/web.php:

Route::middleware('auth')->group(function () {
Route::get('products', [ProductController::class, 'index'])->name('products.index');
 
Route::middleware('is_admin')->group(function () {
Route::get('products/create', [ProductController::class, 'create'])->name('products.create');
Route::post('products', [ProductController::class, 'store'])->name('products.store');
Route::get('products/{product}/edit', [ProductController::class, 'edit'])->name('products.edit');
Route::put('products/{product}', [ProductController::class, 'update'])->name('products.update');
});
});

We will test the product update in the next lesson, but the routes must be prepared now because we must use it in the form.

resources/views/products/edit.blade.php:

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Edit product') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="overflow-hidden overflow-x-auto p-6 bg-white border-b border-gray-200">
<div class="min-w-full align-middle">
<form method="POST" action="{{ route('products.update', $product) }}">
@csrf
@method('PUT')
 
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="$product->name" required />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
 
<div class="mt-4">
<x-input-label for="price" :value="__('Price')" />
<x-text-input id="price" class="block mt-1 w-full" type="text" name="price" :value="$product->price" required />
<x-input-error :messages="$errors->get('price')" class="mt-2" />
</div>
 
<div class="flex items-center mt-4">
<x-primary-button>
{{ __('Save') }}
</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

resources/views/products/index.blade.php:

// ...
<table class="min-w-full divide-y divide-gray-200 border">
<thead>
<tr>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Name</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Price (USD)</span>
</th>
<th class="px-6 py-3 bg-gray-50 text-left">
<span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Price (EUR)</span>
</th>
@if (auth()->user()->is_admin)
<th class="px-6 py-3 bg-gray-50 text-left">
</th>
@endif
</tr>
</thead>
 
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
@forelse($products as $product)
<tr class="bg-white">
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $product->name }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ number_format($product->price, 2) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ $product->price_eur }}
</td>
@if (auth()->user()->is_admin)
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
<a href="{{ route('products.edit', $product) }}" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150">
Edit
</a>
</td>
@endif
</tr>
@empty
<tr class="bg-white">
<td colspan="2" class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-900">
{{ __('No products found') }}
</td>
</tr>
@endforelse
</tbody>
</table>
// ...

products table with edit button

When we go to the edit page, the values in the inputs will be tested.

edit product form


The Test

Now, let's create a test. In the test, we will first use the Factory to create a product and assign it to a variable. Then acting as admin, we will visit the edit page for that product and, from the response, will make a first assert that the status is 200.

tests/Feature/ProductsTest.php:

class ProductsTest extends TestCase
{
// ...
 
public function test_product_edit_contains_correct_values()
{
$product = Product::factory()->create();
 
$response = $this->actingAs($this->admin)->get('products/' . $product->id . '/edit');
 
$response->assertStatus(200);
}
 
private function createUser(bool $isAdmin = false): User
{
return User::factory()->create([
'is_admin' => $isAdmin,
]);
}
}

Next, because the response returns the HTML, we can use assertSee to assert that there is an input with a product name and price value. We must add a second parameter as false so that quotes won't be escaped and show as special chars.

tests/Feature/ProductsTest.php:

class ProductsTest extends TestCase
{
// ...
 
public function test_product_edit_contains_correct_values()
{
$product = Product::factory()->create();
 
$response = $this->actingAs($this->admin)->get('products/' . $product->id . '/edit');
 
$response->assertStatus(200);
$response->assertSee('value="' . $product->name . '"', false);
$response->assertSee('value="' . $product->price . '"', false);
}
 
private function createUser(bool $isAdmin = false): User
{
return User::factory()->create([
'is_admin' => $isAdmin,
]);
}
}

And let's add another assertion to check that View has variable product with the same values.

tests/Feature/ProductsTest.php:

class ProductsTest extends TestCase
{
// ...
 
public function test_product_edit_contains_correct_values()
{
$product = Product::factory()->create();
 
$response = $this->actingAs($this->admin)->get('products/' . $product->id . '/edit');
 
$response->assertStatus(200);
$response->assertSee('value="' . $product->name . '"', false);
$response->assertSee('value="' . $product->price . '"', false);
$response->assertViewHas('product', $product);
}
 
private function createUser(bool $isAdmin = false): User
{
return User::factory()->create([
'is_admin' => $isAdmin,
]);
}
}

product edit form test


Commit for this lesson

Previous: New Product: testing that record was saved into database
avatar

Hello,Thanks for the class, do you have any test examples like the one in this lesson but with Livewire? and with 2 cases, one like this that the values appear in the Edit view and another if they are inside a modal

avatar

I don't have any specific examples for Livewire, sorry, I didn't cover that topic because I guess it's all well covered in Livewire documentation itself.

avatar
You can use Markdown
avatar

What happens if you use vue for the frontend, would this work anyway?

avatar

It depends on what you want to test. Laravel testing does not test JavaScript code directly, so probably it wouldn't work the same way.

avatar

maybe you can use browser test https://laravel.com/docs/10.x/dusk

avatar
You can use Markdown
avatar
You can use Markdown