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 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.
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.
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.
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.
Notice: when using setUp()
to add global variables, don't forget to add parent::setUp()
as a first line of code.