Back

Testing


Introduction

For automated testing, we use a framework Pest because of its many benefits. Pest focuses on simplicity and writing tests is really enjoyable with it.

Additionally, it also has better assertions, cool syntax, and great documentation.

Create command: php artisan make:test Actions/VerifyUserActionTest

1it('has a welcome page', function() {
2 $response = $this->get('/');
3 
4 expect($response->status())->toBe(200);
5});

Why write tests?

  • Code quality - writing tests forces you to think about the code
  • Bugs catching - tests can reveal some bugs before the application is live
  • Time saving - in the long term, tests should reduce the need for manual testing
  • Increased confidence - it is easier to make changes and add new features because there is less risk of breaking functionality.

When to write tests?

Each created class containing some logic should also have a test.

What should be tested?

  • Code logic (actions, support classes, jobs…)
  • Livewire components / Controllers
  • Routes

How to write tests?

On most projects, we only write Feature tests. However, on some complex ones, it is crucial to include also Unit tests.

When writing tests, it is important to include multiple scenarios to cover most of edge cases. Additionally, tests should only contain assertions that are directly related to the tested class or function to avoid redundancy and improve readability.

Pro tip: Keep your coverage as high as possible, it should be at least 70%.

Helpers

Helpers can reduce code duplication and improve test readability. You can use Pest hooks to prepare/clean data before/after each/all test runs. Additionally, you can create custom methods that can be used in multiple tests.

Pest hooks

  • beforeEach() - Prepare something before each test run…
  • afterEach() - Clear testing data after each test run…
  • beforeAll() - Prepare something once before any of this file's tests run…
  • afterAll() - Clear testing data after all tests run…

Custom methods

These methods can be written in a single test suite or directly in Pest.php to access them globally.

Example of local helper in a single test suite:

1function asAdmin(): User
2{
3 $user = User::factory()->create(['admin' => true]);
4 
5 return test()->actingAs($user);
6}
7 
8it('can manage users', function () {
9 asAdmin()->get('/users')->assertOk();
10})

Example of global helper in Pest.php:

1function mockPayments(): object
2{
3 $client = Mockery::mock(PaymentClient::class);
4 
5 return $client;
6}
7 
8it('may buy a book', function () {
9 $client = mockPayments();
10})

Mocking

Mocking certain parts of your application can be necessary for some cases, such as events, file uploads, or 3rd party integrations. More here.

Example of request mocking:

1Http::fake([
2 'google.com/*' => Http::response('foo@gmail.com', 200)
3]);
4 
5$response = resolve(FetchGoogleUserEmailAction::class)->run();
6 
7expect($response)->toBe('foo@gmail.com');

Example of class mocking:

1use function Pest\Laravel\mock;
2 
3mock(Client::class)
4 // You may need to check the passed arguments
5 ->withArgs(fn ($givenOrder) => $givenOrder->is($order))
6 // Or check the response
7 ->andReturn(true)
8 ->shouldReceive('acceptOrder')
9 // You also can check how many times the class should be called
10 ->never()
11 ->once()
12 ->twice()
13 ->times(3)

Pro tip: In case you create a mock for external API integration, you should also ensure that you don't hit official endpoints with your tests - in Laravel you can prevent these stray requests with Http::preventStrayRequests(). After calling this method, each request without matching fake throws an exception. Read more

Fixtures

Fixtures are static files used for testing, and they should be placed inside the "fixtures" directory. They can be used in tests to simulate real data sources or to provide specific test conditions.

Usage examples

1$payment = base_path('tests/fixtures/payment.xml');
2 
3app(ProcessPaymentAction::class)->run($payment, ...);

Naming

To ensure the proper naming of your test cases, it is recommended that you follow the example provided.

1// Good examples - lowercase, descriptive
2test('payment can be processed', function() {
3 ...
4});
5 
6it('has a welcome page', function() {
7 ...
8});
9 
10// Bad example - uppercase, low descriptive
11test('PAYMENT', function() {
12 ...
13});
14 
15it('sums', function() {
16 ...
17});
Edit this page

We are hiring!