Back to Course |
Testing in Laravel 11 For Beginners

Refactor: Repeating Test Code into Hooks or setUp

In this lesson, let's see how to avoid repeating some code in the tests.

Now, we use a Factory to create a user in every test. We will extract this part using Pest hooks.


Pest Hooks

Pest offers four hook methods:

  • beforeEach()
  • afterEach()
  • beforeAll()
  • afterAll()

Each has a name that tells when it is being executed. For example, to create a user before the test is run, we can use the beforeEach() hook.

tests/Feature/ProductsTest.php:

beforeEach(function () {
$this->user = User::factory()->create();
});
 
// ...

Now, instead of creating a user in every test, we can use $this->user to pass it into the actingAs() function.

tests/Feature/ProductsTest.php:

beforeEach(function () {
$this->user = User::factory()->create();
});
 
test('homepage contains empty table', function () {
$user = User::factory()->create();
 
actingAs($user)
actingAs($this->user)
->get('/products')
->assertStatus(200)
->assertSee(__('No products found'));
});
 
test('homepage contains non empty table', function () {
$user = User::factory()->create();
$product = Product::create([
'name' => 'Product 1',
'price' => 123,
]);
 
actingAs($user)
actingAs($this->user)
->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 () {
$user = User::factory()->create();
$products = Product::factory(11)->create();
$lastProduct = $products->last();
 
actingAs($user)
actingAs($this->user)
->get('/products')
->assertStatus(200)
->assertViewHas('products', function (LengthAwarePaginator $collection) use ($lastProduct) {
return $collection->doesntContain($lastProduct);
});
});

Tests are still green.

tests refactor user creation


PHPUnit examples

With PHPUnit, we can do this refactoring in two ways.

The first is a general PHP way, and the second is setting up with PHPUnit and Laravel testing logic.

Option 1. Private Method.

For the method, let's create a new private function where we will create a new user and return its Model.

tests/Feature/ProductsTest.php:

use App\Models\User;
 
class ProductsTest extends TestCase
{
use RefreshDatabase;
 
public function test_homepage_contains_empty_table(): void
{
$user = User::factory()->create();
$user = $this->createUser();
 
$response = $this->actingAs($user)->get('/products');
 
$response->assertStatus(200);
$response->assertSee(__('No products found'));
}
 
public function test_homepage_contains_non_empty_table(): void
{
$product = Product::create([
'name' => 'Product 1',
'price' => 123,
]);
 
$user = User::factory()->create();
$user = $this->createUser();
 
$response = $this->actingAs($user)->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();
 
$user = User::factory()->create();
$user = $this->createUser();
 
$response = $this->actingAs($user)->get('/products');
 
$response->assertStatus(200);
$response->assertViewHas('products', function (LengthAwarePaginator $collection) use ($lastProduct) {
return $collection->doesntContain($lastProduct);
});
}
 
private function createUser(): User
{
return User::factory()->create();
}
}

But the code didn't get much shorter, and this is more beneficial if you have more logic inside that createUser() method, like a few more lines of code related to the same user.


Option 2. Constructor: setUp() method.

A better way to refactor this code would be to have some constructor.

In tests, the constructor is in a method called setUp(). In the setUp(), we can assign a user to a private property and call this property where we need it.

tests/Feature/ProductsTest.php:

use App\Models\User;
 
class ProductsTest extends TestCase
{
use RefreshDatabase;
 
private User $user;
 
protected function setUp(): void
{
parent::setUp();
 
$this->user = $this->createUser();
}
 
public function test_homepage_contains_empty_table(): void
{
$user = $this->createUser();
 
$response = $this->actingAs($user)->get('/products');
$response = $this->actingAs($this->user)->get('/products');
 
$response->assertStatus(200);
$response->assertSee(__('No products found'));
}
 
public function test_homepage_contains_non_empty_table(): void
{
$product = Product::create([
'name' => 'Product 1',
'price' => 123,
]);
 
$user = $this->createUser();
 
$response = $this->actingAs($user)->get('/products');
$response = $this->actingAs($this->user)->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();
 
$user = $this->createUser();
 
$response = $this->actingAs($user)->get('/products');
$response = $this->actingAs($this->user)->get('/products');
 
$response->assertStatus(200);
$response->assertViewHas('products', function (LengthAwarePaginator $collection) use ($lastProduct) {
return $collection->doesntContain($lastProduct);
});
}
 
private function createUser(): User
{
return User::factory()->create();
}
}

Let's re-run the tests to make sure everything works.

tests refactor user creation

Notice: when using setUp() to add global variables, don't forget to add parent::setUp() as a first line of code.