In this lesson, let's talk about fake data. Remember the "Product 1" name? So yeah, we shouldn't need to hardcode these values: we can use Eloquent factories to quickly generate fake data for tests.
Let's imagine we want to test pagination. Our table would contain ten records with pagination, and you want to test that pagination works. We will need to create eleven records and test that the last record isn't shown on the first page.
First, in the Controller, we need to change to use pagination.
app/Http/Controllers/ProductController.php:
class ProductController extends Controller{ public function index(): View { $products = Product::all(); $products = Product::paginate(10); return view('products.index', compact('products')); }}
And the test.
tests/Feature/ProductsTest.php:
use App\Models\Product;use Illuminate\Database\Eloquent\Collection;use Illuminate\Pagination\LengthAwarePaginator;use function Pest\Laravel\get; test('homepage contains empty table', function () { get('/products') ->assertStatus(200) ->assertSee(__('No products found'));}); test('homepage contains non empty table', function () { $product = Product::create([ 'name' => 'Product 1', 'price' => 123, ]); get('/products') ->assertStatus(200) ->assertDontSee(__('No products found')) ->assertSee('Product 1') ->assertViewHas('products', function (LengthAwarePaginator $collection) use ($product) { return $collection->contains($product); });}); test('paginated products table doesnt contain 11th record', function () { for ($i = 1; $i <= 11; $i++) { $product = Product::create([ 'name' => 'Product ' . $i, 'price' => rand(100, 999), ]); } get('/products') ->assertStatus(200) ->assertViewHas('products', function (LengthAwarePaginator $collection) use ($product) { return $collection->doesntContain($product); });});
In this example, we used the PHP for
loop in the test to create eleven records. Then, we go to the /products
URL and assert that the collection doesn't contain the last product. Now, let's run the tests.
The tests are green, but creating the records with the for
loop isn't convenient. Wouldn't it be cool to just quickly generate 11 fake products in one line of code?
Here's where the Factories should be used.
You can find factories in a database/factories
directory. Laravel skeleton comes with one default UserFactory
, so we can analyze that:
database/factories/UserFactory.php:
class UserFactory extends Factory{ public function definition(): array { return [ 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => Str::random(10), ]; } public function unverified(): static { return $this->state(fn (array $attributes) => [ 'email_verified_at' => null, ]); }}
You specify the rules of different fields in the definition()
method. You may use a fake()
helper to fake some data. You can check what Faker methods exist in the official documentation.
With those rules specified, you can generate a fake User model by running App\Models\User::factory()->create();
anywhere in your Laravel application, not necessarily in the tests.
Want 10 fake users? Easy:
User::factory(10)->create();
Now you see the benefits of Factories?
Now, let's create a Factory for a Product
Model.
php artisan make:factory ProductFactory
And in the ProductFactory
, we need to provide rules for two fields.
database/factories/ProductFactory.php:
class ProductFactory extends Factory{ public function definition(): array { return [ 'name' => fake()->text(), 'price' => rand(100, 999), ]; }}
To use the Factory Model, it needs to have a HasFactory
trait.
app/Models/Product.php:
use Illuminate\Database\Eloquent\Factories\HasFactory; class Product extends Model{ use HasFactory; // ...}
Then, we can replace the for loop with a Factory in the test.
tests/Feature/ProductsTest.php:
// ... test('paginated products table doesnt contain 11th record', function () { for ($i = 1; $i <= 11; $i++) { $product = Product::create([ 'name' => 'Product ' . $i, 'price' => rand(100, 999), ]); } Product::factory(11)->create(); get('/products') ->assertStatus(200) ->assertViewHas('products', function (LengthAwarePaginator $collection) use ($product) { return $collection->doesntContain($product); });});
Shorter code, right?
But now we introduced a problem: we don't have the $product
variable for our 11th product. Here, we can assign the Factory method result to a variable, which is a Laravel Collection. Then, we can use the last()
Collection method to get the last record.
tests/Feature/ProductsTest.php:
// ... test('paginated products table doesnt contain 11th record', function () { $products = Product::factory(11)->create(); $lastProduct = $products->last(); get('/products') ->assertStatus(200) ->assertViewHas('products', function (LengthAwarePaginator $collection) use ($lastProduct) { return $collection->doesntContain($lastProduct); });});
After relaunching the tests, everything should still be green.
In addition to creating general rules in Factories, you can overwrite some of those values. You can pass an array of parameters in the create()
method.
For example, if you want to hardcode the price:
tests/Feature/ProductsTest.php:
$products = Product::factory(11)->create([ 'price' => 9999,]);
All products will be created with this price 9999
instead of the fake value set in the Factory.
In this case, the PHPUnit test syntax is almost identical to the Pest test.
tests/Feature/ProductsTest.php:
use App\Models\Product;use Illuminate\Pagination\LengthAwarePaginator;use Illuminate\Foundation\Testing\RefreshDatabase; class ProductsTest extends TestCase{ use RefreshDatabase; // ... public function test_homepage_contains_non_empty_table(): void { $product = Product::create([ 'name' => 'Product 1', 'price' => 123, ]); $response = $this->get('/products'); $response->assertStatus(200); $response->assertDontSee(__('No products found')); $response->assertSee('Product 1'); $response->assertViewHas('products', function (LengthAwarePaginator $collection) use ($product) { return $collection->contains($product); }); } public function test_paginated_products_table_doesnt_contain_11th_record() { $products = Product::factory(11)->create(); $lastProduct = $products->last(); $response = $this->get('/products'); $response->assertStatus(200); $response->assertViewHas('products', function (LengthAwarePaginator $collection) use ($lastProduct) { return $collection->doesntContain($lastProduct); }); }}