CoderFunda
  • Home
  • About us
    • Contact Us
    • Disclaimer
    • Privacy Policy
    • About us
  • Home
  • Php
  • HTML
  • CSS
  • JavaScript
    • JavaScript
    • Jquery
    • JqueryUI
    • Stock
  • SQL
  • Vue.Js
  • Python
  • Wordpress
  • C++
    • C++
    • C
  • Laravel
    • Laravel
      • Overview
      • Namespaces
      • Middleware
      • Routing
      • Configuration
      • Application Structure
      • Installation
    • Overview
  • DBMS
    • DBMS
      • PL/SQL
      • SQLite
      • MongoDB
      • Cassandra
      • MySQL
      • Oracle
      • CouchDB
      • Neo4j
      • DB2
      • Quiz
    • Overview
  • Entertainment
    • TV Series Update
    • Movie Review
    • Movie Review
  • More
    • Vue. Js
    • Php Question
    • Php Interview Question
    • Laravel Interview Question
    • SQL Interview Question
    • IAS Interview Question
    • PCS Interview Question
    • Technology
    • Other

19 August, 2022

Test form requests in Laravel with Request Factories by Worksome

 Programing Coderfunda     August 19, 2022     Laravel, Packages     No comments   

 A month or two ago, I was busy writing tests for a Laravel app. More specifically, I was testing a Laravel post request...

  • it allows a user to sign up
  • it requires a valid email
  • it does not allow a user to sign up twice
  • it allows a user to fill in address details

You get the picture. The more tests for this sign up flow I created, the more frustrated I became. Why? Because I had to keep repeating myself over and over and over again. See, any request worth its salt is going to have validation. Because of that, even if our test only deals with a small subset of fields in the request, we have to include everything!

Let's take the case of 'it allows a user to fill in address details'. I'm actually interested in perhaps 4 fields: address_line_1, address_line_2, city, and postcode. Buuuuuut, because of validation I have to pass email, first_name, last_name, telephone, accepts_terms and every other required fields.

1it('allows a user to fill in address details', function () {
2 $this->post('/signup', [
3 'first_name' => 'Luke', // Don't care
4 'last_name' => 'Downing', // Not what I'm testing
5 'email' => 'foo@bar.com', // Sigh
6 'telephone' => '01234567890', // Okay, I'm getting annoyed now
7 'accepts_terms' => true, // Are we there yet?
8 'address_line_1' => '1 Test Street', // Finally!
9 'address_line_2' => 'Test Area',
10 'city' => 'Testerfield',
11 'postcode' => 'T35T 1NG',
12 ]);
13 
14 expect(User::latest()->first())
15 ->address_line_1->toBe('1 Test Street')
16 ->address_line_2->toBe('Test Area')
17 ->city->toBe('Testerfield')
18 ->postcode->toBe('T35T 1NG');
19});

This problem only gets more frustrating as we add more tests. And that's a real issue, because it can actually lead to not writing tests at all! Further, imagine adding a new required field to this form 6 months down the line. We're going to have to update all of these tests and add more data that doesn't add value to the test just to make them pass.

This got me thinking. See, we had a similar problem with eloquent models a while back. But we solved that with model factories: classes that would do all of the laborious work for you so that you could just write User::factory()->create() in your test. Why not apply that same approach to requests?

1it('allows a user to fill in address details', function () {
2 SignupRequest::fake();
3 
4 $this->post('/signup', [
5 'address_line_1' => '1 Test Street',
6 'address_line_2' => 'Test Area',
7 'city' => 'Testerfield',
8 'postcode' => 'T35T 1NG',
9 ]);
10 
11 expect(User::latest()->first())
12 ->address_line_1->toBe('1 Test Street')
13 ->address_line_2->toBe('Test Area')
14 ->city->toBe('Testerfield')
15 ->postcode->toBe('T35T 1NG');
16});

Oh, that's so much nicer. It's shorter, easier to write, simpler to maintain and conveys the purpose of the test without uncecessary detail. Perfection!

So, without further ado, I'm pleased to present Request Factories by Worksome!

Setting up our request factory

The package ships with an artisan command to easily generate new factories for your project. You can pass the desired name of your factory, or the Fully Qualified Class Name (FQCN) of a form request from your app.

It's important to note that form requests are completely optional. Request factories work just fine with standard requests too.

1# Using a custom name
2php artisan make:request-factory SignupRequestFactory
3 
4# Using a form request FQCN
5php artisan make:request-factory "App\Http\Requests\SignupRequest"

By default, your factory will be placed under tests/RequestFactories. When you open your new request factory, you'll notice a definition method that returns an array. Much like model factories, this is where we define any attributes we want to be present when faking a request.

My recommendation is that you only include the minimum number of attributes to complete a valid request in the definition method, and use methods to decorate your factory as desired.

1class SignupRequestFactory extends RequestFactory
2{
3 public function definition(): array
4 {
5 return [
6 'email' => $this->faker->safeEmail,
7 'first_name' => $this->faker->firstName,
8 'last_name' => $this->faker->lastName,
9 'telephone' => '01234567890',
10 'accepts_terms' => true,
11 ];
12 }
13}

Note that request factories have a $faker property we can access to create randomised data rather than hardcoding values. With our required fields defined, we can now fake the data in our request.

1it('allows a user to fill in address details', function () {
2 SignupRequestFactory::new()->fake();
3 
4 $this->post('/signup', [
5 'address_line_1' => '1 Test Street',
6 'address_line_2' => 'Test Area',
7 'city' => 'Testerfield',
8 'postcode' => 'T35T 1NG',
9 ]);
10 
11 expect(User::latest()->first())
12 ->address_line_1->toBe('1 Test Street')
13 ->address_line_2->toBe('Test Area')
14 ->city->toBe('Testerfield')
15 ->postcode->toBe('T35T 1NG');
16});

One cool (although perhaps divisive) feature of eloquent model factories is the ability to create a new factory instance directly from a model: User::factory(). I wanted to bring a little bit of that magic to request factories too, so there is a Worksome\RequestFactories\Concerns\HasFactory trait available that you may add to your form requests if you'd like. Once added, you fake directly from the form request:

1it('allows a user to fill in address details', function () {
2 SignupRequest::fake();
3 
4 $this->post('/signup', [
5 'address_line_1' => '1 Test Street',
6 'address_line_2' => 'Test Area',
7 'city' => 'Testerfield',
8 'postcode' => 'T35T 1NG',
9 ]);
10 
11 expect(User::latest()->first())
12 ->address_line_1->toBe('1 Test Street')
13 ->address_line_2->toBe('Test Area')
14 ->city->toBe('Testerfield')
15 ->postcode->toBe('T35T 1NG');
16});

Of course, this is completely optional, and only really makes sense if you make use of form requests in your application. But it's a nice-to-have if that's your style.

Adding methods to our factory

One of the great things about factories is the ability to add methods that transform factory state. For example, let's say we have a test for postcode formatting. We need to provided other address fields to create a valid request, so why not add a withAddress() method to our factory?

1class SignupRequestFactory extends RequestFactory
2{
3 public function definition(): array
4 {
5 return [
6 'email' => $this->faker->safeEmail,
7 'first_name' => $this->faker->firstName,
8 'last_name' => $this->faker->lastName,
9 'telephone' => '01234567890',
10 'accepts_terms' => true,
11 ];
12 }
13 
14 public function withAddress(): self
15 {
16 return $this->state([
17 'address_line_1' => '1 Test Street',
18 'address_line_2' => 'Test Area',
19 'city' => 'Testerfield',
20 'postcode' => 'T35T 1NG',
21 ]);
22 }
23}

You'll note the state method, similar to Laravel's model factories. We've tried to keep the APIs very similar, so it will feel immediately familiar. Now, let's go write our test:

1it('correctly formats the postcode', function () {
2 SignupRequestFactory::new()->withAddress()->fake();
3 
4 $this->post('/signup', ['postcode' => 'T3 5T1N G']);
5 
6 expect(User::latest()->first()->postcode)->toBe('T35T 1NG')
7});

How nice is that? Our test maintains its focus; we only have to provide the field we're actually testing. Also, note that any data you pass to post (or any of the Laravel request testing methods for that matter), will override data defined in the factory.

Other goodies

We're really just scratching the surface of what's possible with request factories in this post. Here's a quick showcase of some other goodies available.

Omitting fields from a request

1it('requires an email', function () {
2 SignupRequestFactory::new()->without(['email'])->fake();
3 
4 $this->post('/signup')->assertInvalid(['email']);
5});

Using nested factories for shared fields

1class MailingRequestFactory extends RequestFactory
2{
3 public function definition(): array
4 {
5 return [
6 'email' => $this->faker->safeEmail,
7 'address' => AddressRequestFactory::new(),
8 ];
9 }
10}

Using closures for lazily defined attributes

1class MailingRequestFactory extends RequestFactory
2{
3 public function definition(): array
4 {
5 return [
6 'first_name' => $this->faker->firstName,
7 'last_name' => $this->faker->lastName,
8 'email' => fn ($attrs) => "{$attrs['first_name']}.{$attr['last_name']}@foo.com",
9 ];
10 }
11}

Using create on a factory

If you don't want to fake a request globally or prefer a syntax closer to model factories, you can call create on a request factory to return an array of input that can then be passed to post or other request methods.

1it('requires an email', function () {
2 $data = SignupRequestFactory::new()->create();
3 
4 $this->post('/signup', $data)->assertInvalid(['email']);
5});

Using the fakeRequest helper with Pest PHP

If you're using Pest PHP for testing, we provide a groovy fakeRequest helper that can be chained onto the end of your test if you prefer.

1it('correctly formats the postcode', function () {
2 $this->post('/signup', ['postcode' => 'T3 5T1N G']);
3 
4 expect(User::latest()->first()->postcode)->toBe('T35T 1NG')
5})
6 ->fakeRequest(SignupRequestFactory::class)
7 ->withAddress();

Wrapping up

For more details on all the things you can do with request factories, be sure to check out the detailed documentation!

I think request factories have huge potential in making testing requests simpler to write, easier to maintain and providing greater clarity on the purpose of each test.

I look forward to seeing how you use them in your projects! Make sure to reach out on Twitter to let me know what you think.

Take care!

  • Share This:  
  •  Facebook
  •  Twitter
  •  Google+
  •  Stumble
  •  Digg
Email ThisBlogThis!Share to XShare to Facebook

Related Posts:

  • Google Natural Language API for Laravel The laravel-natural-language package makes using the Google Natural Language Processing (NLP) API in your Laravel app a breeze with mi… Read More
  • Notion API for Laravel Notion API for Laravel is a package to effortlessly create Notion integrations with Laravel:This package provides a simple and crisp w… Read More
  • Laravel Nova Inline Select Laravel Nova Inline Select is a package that allows you to change the status of a resource directly from the index or detail views."We have some… Read More
  • ElasticSearch Driver for Laravel Scout Explorer is a next-gen Elasticsearch driver for Laravel Scout with the power of Elasticsearch’s queries. It provides a … Read More
  • Sidecar packages, deploys, and executes AWS Lambda functions from your Laravel application. Sidecar is a package by the Hammerstone team that takes the pain out of the packaging, deploying, and executing of AWS Lambd… Read More
Newer Post Older Post Home

0 comments:

Post a Comment

Thanks

Meta

Popular Posts

  • Spring boot app (error: method getFirst()) failed to run at local machine, but can run on server
    The Spring boot app can run on the online server. Now, we want to replicate the same app at the local machine but the Spring boot jar file f...
  • Log activity in a Laravel app with Spatie/Laravel-Activitylog
      Requirements This package needs PHP 8.1+ and Laravel 9.0 or higher. The latest version of this package needs PHP 8.2+ and Laravel 8 or hig...
  • Laravel auth login with phone or email
          <?php     Laravel auth login with phone or email     <? php     namespace App \ Http \ Controllers \ Auth ;         use ...
  • Failed to install 'cordova-plugin-firebase': CordovaError: Uh oh
    I had follow these steps to install an configure firebase to my cordova project for cloud messaging. https://medium.com/@felipepucinelli/how...
  • Vue3 :style backgroundImage not working with require
    I'm trying to migrate a Vue 2 project to Vue 3. In Vue 2 I used v-bind style as follow: In Vue 3 this doesn't work... I tried a...

Categories

  • Ajax (26)
  • Bootstrap (30)
  • DBMS (42)
  • HTML (12)
  • HTML5 (45)
  • JavaScript (10)
  • Jquery (34)
  • Jquery UI (2)
  • JqueryUI (32)
  • Laravel (1017)
  • Laravel Tutorials (23)
  • Laravel-Question (6)
  • Magento (9)
  • Magento 2 (95)
  • MariaDB (1)
  • MySql Tutorial (2)
  • PHP-Interview-Questions (3)
  • Php Question (13)
  • Python (36)
  • RDBMS (13)
  • SQL Tutorial (79)
  • Vue.js Tutorial (68)
  • Wordpress (150)
  • Wordpress Theme (3)
  • codeigniter (108)
  • oops (4)
  • php (853)

Social Media Links

  • Follow on Twitter
  • Like on Facebook
  • Subscribe on Youtube
  • Follow on Instagram

Pages

  • Home
  • Contact Us
  • Privacy Policy
  • About us

Blog Archive

  • September (100)
  • August (50)
  • July (56)
  • June (46)
  • May (59)
  • April (50)
  • March (60)
  • February (42)
  • January (53)
  • December (58)
  • November (61)
  • October (39)
  • September (36)
  • August (36)
  • July (34)
  • June (34)
  • May (36)
  • April (29)
  • March (82)
  • February (1)
  • January (8)
  • December (14)
  • November (41)
  • October (13)
  • September (5)
  • August (48)
  • July (9)
  • June (6)
  • May (119)
  • April (259)
  • March (122)
  • February (368)
  • January (33)
  • October (2)
  • July (11)
  • June (29)
  • May (25)
  • April (168)
  • March (93)
  • February (60)
  • January (28)
  • December (195)
  • November (24)
  • October (40)
  • September (55)
  • August (6)
  • July (48)
  • May (2)
  • January (2)
  • July (6)
  • June (6)
  • February (17)
  • January (69)
  • December (122)
  • November (56)
  • October (92)
  • September (76)
  • August (6)

  • Failed to install 'cordova-plugin-firebase': CordovaError: Uh oh - 9/21/2024
  • pyspark XPath Query Returns Lists Omitting Missing Values Instead of Including None - 9/20/2024
  • SQL REPL from within Python/Sqlalchemy/Psychopg2 - 9/20/2024
  • MySql Explain with Tobias Petry - 9/20/2024
  • How to combine information from different devices into one common abstract virtual disk? [closed] - 9/20/2024

Laravel News

  • Auto-translate Application Strings with Laratext - 5/16/2025
  • Simplify Factory Associations with Laravel's UseFactory Attribute - 5/13/2025
  • Improved Installation and Frontend Hooks in Laravel Echo 2.1 - 5/15/2025
  • Filter Model Attributes with Laravel's New except() Method - 5/13/2025
  • Arr::from() Method in Laravel 12.14 - 5/14/2025

Copyright © 2025 CoderFunda | Powered by Blogger
Design by Coderfunda | Blogger Theme by Coderfunda | Distributed By Coderfunda