Back to Course |
Testing in Laravel 9 For Beginners: PHPUnit, Pest, TDD

Factories: How to create many testing records without loops

In this lesson, let's see how we can easily use Eloquent factories to create fake data for tests easily.

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 showing on the 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\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()
{
for ($i = 1; $i <= 11; $i++) {
$product = Product::create([
'name' => 'Product ' . $i,
'price' => rand(100, 999),
]);
}
 
$response = $this->get('/products');
 
$response->assertStatus(200);
$response->assertViewHas('products', function (LengthAwarePaginator $collection) use ($product) {
return $collection->doesntContain($product);
});
}
}

We used the PHP function for in the test to create eleven records. Then we go to /products URL and assert that the collection doesn't contain the last product. Now, let's run the tests.

products pagination tests

The tests are green, but creating the for loop isn't convenient. Here is where the Factories should be used.

You can find factories in a database/factories directory. With Laravel comes one default Factory, UserFactory. You specify the rules of different fields in the definition method. You have a fake() helper to fake some data because you don't care what that name is in the tests. What faker has formatters you can check in the official documentation.

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,
]);
}
}

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:

use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
 
class ProductsTest extends TestCase
{
use RefreshDatabase;
 
// ...
 
public function test_paginated_products_table_doesnt_contain_11th_record()
{
Product::factory(11)->create();
 
for ($i = 1; $i <= 11; $i++) {
$product = Product::create([
'name' => 'Product ' . $i,
'price' => rand(100, 999),
]);
}
 
$response = $this->get('/products');
 
$response->assertStatus(200);
$response->assertViewHas('products', function (LengthAwarePaginator $collection) use ($product) {
return $collection->doesntContain($product);
});
}
}

But now we don't have the $product variable. Here, we can assign Factory to a variable, a collection, and use the last method to get the last record.

tests/Feature/ProductsTest.php:

class ProductsTest extends TestCase
{
use RefreshDatabase;
 
// ...
 
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 ($product) {
$response->assertViewHas('products', function (LengthAwarePaginator $collection) use ($lastProduct) {
return $collection->doesntContain($product);
return $collection->doesntContain($lastProduct);
});
}
}

After relaunching the tests, everything should still be green.

products pagination tests

Also, knowing that you can overwrite the values is very useful. In the create, you can pass an array of parameters. For example, if you would want to hardcode the price.

tests/Feature/ProductsTest.php:

class ProductsTest extends TestCase
{
use RefreshDatabase;
 
// ...
 
public function test_paginated_products_table_doesnt_contain_11th_record()
{
$products = Product::factory(11)->create([
'price' => 9999,
]);
 
// ...
}
}

All products will be created with this price instead of one set in the Factory.


Commit for this lesson