Testing Laravel Form Requests in a different way

Photo by Shirly Niv Marton on UnsplashTesting Laravel Form Requests in a different wayDaanBlockedUnblockFollowFollowingApr 16Many 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 requestFor this example I will be making a form request to save a product.

php artisan make:request SaveProductRequestThe generated file class will be placed in App/Http/Requests.

We will declare a set of validation rules for this form request:The title parameter should be a string with a maximum of 50 characters.

The price parameter should be numeric.

Those are the only two validation rules for now.

This is what the SaveProductRequest class looks like:<?phpnamespace AppHttpRequests;use IlluminateFoundationHttpFormRequest;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 modelLet’s create a Product model:php artisan make:model Models/Product -mThe migration file looks like this:<?phpuse IlluminateSupportFacadesSchema;use IlluminateDatabaseSchemaBlueprint;use IlluminateDatabaseMigrationsMigration;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 routeLet’s set up the ProductController:php artisan make:controller ProductControllerAnd give it a very simple implementation:<?phpnamespace AppHttpControllers;use AppHttpRequestsSaveProductRequest;use AppHttpResourcesProduct as ProductResource;use AppModelsProduct;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 ProductAnd finally, add a route to your routes/api.

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

store');Writing the testsBefore we can start making our tests we have to create a testfile:php artisan make:test App/Http/Requests/SaveProductRequestTestNote: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:<?phpnamespace TestsFeatureAppHttpRequests;use AppUser;use IlluminateHttpResponse;use TestsTestCase;use IlluminateFoundationTestingWithFaker;use IlluminateFoundationTestingRefreshDatabase;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 providerPHPUnit’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:<?phpnamespace TestsFeatureAppHttpRequests;use AppHttpRequestsSaveProductRequest;use FakerFactory;use TestsTestCase;use IlluminateFoundationTestingRefreshDatabase;class SaveProductRequestTest extends TestCase{ use RefreshDatabase; /** @var AppHttpRequestsSaveProductRequest */ private $rules; /** @var IlluminateValidationValidator */ 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 passThe 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.

.. More details

Leave a Reply