Photo by Shirly Niv Marton on Unsplash

Testing Laravel Form Requests in a different way

Daan
Daan
Apr 16 · 6 min read

Many developers are struggling with testing form requests in an efficient way. Most of the time you’ll end up with writing a seperate unit test for each rule that is defined in your form request. This results in a lot of tests like test_request_without_title and test_request_without_content. All of these methods are implemented in exactly the same way, only calling your endpoint with some different data. This will result in a lot of duplicate code. In this guide I will show you a different way to test your form request, which I think is more clean and improves the maintainability of your tests.

Creating a form request

For this example I will be making a form request to save a product.

php artisan make:request SaveProductRequest

The generated file class will be placed in App/Http/Requests.

We will declare a set of validation rules for this form request:

  1. The title parameter should be a string with a maximum of 50 characters.
  2. The price parameter should be numeric.

Those are the only two validation rules for now.

This is what the SaveProductRequest class looks like:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class SaveProductRequest extends FormRequest
{
public function authorize()
{
return true;
}

public function rules()
{
return [
'title' => 'required|string|max:50',
'price' => 'required|numeric',
];
}
}

Within the authorize method you can check if the user has permission to perform this request. For example, you could check if the user is an admin, but for now anybody is allowed to perform this request.

Setting up the model

Let’s create a Product model:

php artisan make:model Models/Product -m

The migration file looks like this:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateProductsTable extends Migration
{
public function up()
{
Schema::create('products', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('title');
$table->double('price');
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('products');
}
}

Setting up the controller and route

Let’s set up the ProductController:

php artisan make:controller ProductController

And give it a very simple implementation:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\SaveProductRequest;
use App\Http\Resources\Product as ProductResource;
use App\Models\Product;

class ProductController extends Controller
{
public function store(SaveProductRequest $request)
{
$product = Product::create($request->validated());

return ProductResource::make($product);
}
}

Note:
The ProductResource is a resource that you can make with:

php artisan make:resource Product

And finally, add a route to your routes/api.php:

Route::post('/products', 'ProductController@store')->name('products.store');

Writing the tests

Before we can start making our tests we have to create a testfile:

php artisan make:test App/Http/Requests/SaveProductRequestTest

Note:
I prefer to structure my tests in this way, but you could choose to leave out the App/Http/Requests folder.

A typical test suite for this controller could look like this:

<?php

namespace Tests\Feature\App\Http\Requests;

use App\User;
use Illuminate\Http\Response;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;

class SaveProductRequestTest extends TestCase
{
use RefreshDatabase, WithFaker;

protected function setUp(): void
{
parent::setUp();

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

/** @test */
public function request_should_fail_when_no_title_is_provided()
{
$response = $this->actingAs($this->user)
->postJson(route('products.store'), [
'price' => $this->faker->numberBetween(1, 50)
]);

$response>assertStatus(
Response::HTTP_UNPROCESSABLE_ENTITY
);

$response->assertJsonValidationErrors('title');
}

/** @test */
public function request_should_fail_when_no_price_is_provided()
{
$response = $this->actingAs($this->user)
->postJson(route('products.store'), [
'title' => $this->faker->word()
]);

$response->assertStatus(
Response::HTTP_UNPROCESSABLE_ENTITY
);

$response->assertJsonValidationErrors('price');
}

/** @test */
public function request_should_fail_when_title_has_more_than_50_characters()
{
$response = $this->actingAs($this->user)
->postJson(route('products.store'), [
'title' => $this->faker->paragraph()
]);

$response->assertStatus(
Response::HTTP_UNPROCESSABLE_ENTITY
);

$response->assertJsonValidationErrors('price');
}

/** @test */
public function request_should_pass_when_data_is_provided()
{
$response = $this->actingAs($this->user)
->postJson(route('products.store'), [
'title' => $this->faker->word(),
'price' => $this->faker->numberBetween(1, 50)
]);

$response->assertStatus(Response::HTTP_CREATED);

$response->assertJsonMissingValidationErrors([
'title',
'price'
]);
}
}

This is how most of the developers would test a form request. This works and all the tests are passing, but there is a lot of duplicate code. The only thing that differs between the tests is the data that gets send to the endpoint. This can be done more efficiently.

Meet PHPUnit’s data provider

PHPUnit’s data provider provides an elegant way to write tests for Laravel’s form requests. A data provider allows you to structure tests once and run them multiple times with different datasets.

A data provider method must be public and return an array or an object that implements the Iterator interface. You can specify the data provider by using the @dataProvider annotation.

The most basic example of a data provider looks like this:

/**
* @dataProvider provider
*/
public
function testAdd($a, $b, $c)
{
$this->assertEquals($c, $a + $b);
}

public function provider()
{
return [
[0, 0, 0],
[0, 1, 1],
[1, 0, 1],
[1, 1, 3]
];
}

For every array in the provider method the testAdd method will be called. The arguments that are being passed to the testAdd method are specified in the array from the provider. So the first call would be testAdd(0, 0, 0) and the second call would be testAdd(0, 1, 1).

How can we use this to test our form request?

Just like we specified the numbers for the testAdd method in a data provider, we could also specify the data which our endpoint gets called with. Then we run each of those data arrays through Laravel’s Validator class to check if the validation rules pass.

What is most important here is the structure of the data provider. In the key of the data provider array we specify the name of the test. Within this array we have two attributes: passed and data. The passed attribute is a boolean with the expected outcome of the validator. The data attribute contains the data that we want to send to the endpoint.

This is what the code will look like:

<?php

namespace Tests\Feature\App\Http\Requests;

use App\Http\Requests\SaveProductRequest;
use Faker\Factory;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class SaveProductRequestTest extends TestCase
{
use RefreshDatabase;

/** @var \App\Http\Requests\SaveProductRequest */
private $rules;

/** @var \Illuminate\Validation\Validator */
private $validator;

public function setUp(): void
{
parent::setUp();

$this->validator = app()->get('validator');

$this->rules = (new SaveProductRequest())->rules();
}

public function validationProvider()
{
/* WithFaker trait doesn't work in the dataProvider */
$faker = Factory::create( Factory::DEFAULT_LOCALE);

return [
'request_should_fail_when_no_title_is_provided' => [
'passed' => false,
'data' => [
'price' => $faker->numberBetween(1, 50)
]
],
'request_should_fail_when_no_price_is_provided' => [
'passed' => false,
'data' => [
'title' => $faker->word()
]
],
'request_should_fail_when_title_has_more_than_50_characters' => [
'passed' => false,
'data' => [
'title' => $faker->paragraph()
]
],
'request_should_pass_when_data_is_provided' => [
'passed' => true,
'data' => [
'title' => $faker->word(),
'price' => $faker->numberBetween(1, 50)
]
]
];
}

/**
*
@test
* @dataProvider validationProvider
*
@param bool $shouldPass
*
@param array $mockedRequestData
*/
public function validation_results_as_expected($shouldPass, $mockedRequestData)
{
$this->assertEquals(
$shouldPass,
$this->validate($mockedRequestData)
);
}

protected function validate($mockedRequestData)
{
return $this->validator
->make($mockedRequestData, $this->rules)
->passes();
}
}

And the tests still pass

The result is the same, all tests still pass, but duplication is reduced and maintainability improved. What do you think about this way of testing your form requests? Do you test your form requests in a different way? Please let me know in the comments.


If you enjoyed this post or if it has helped you testing your code make sure to check out my other posts aswell. Please feel free to leave a comment if you have any feedback, questions or want me to write about another Laravel related topic.

Daan

Written by

Daan

Backend developer from The Netherlands. Crypto enthusiast.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade