Laravel: Testing


Laravel is built supporting testing with Pest and PHPUnit out of the box, including a phpunit.xml file.

A Laravel application contains two directories by default inside the tests folder. These are Feature and Unit.

On the one hand, unit tests focus in a single class and do not boot the Laravel application, so they have no access to the database or other services.

On the other hand, feature tests are focused on the interaction between several objects.

Laravel will use the testing environment and configure the session and cache to the array driver (in memory) by default when running tests, so they will not be persisted.

Creating a test

We can use the the next artisan command to create a feature test.

php artisan make:test MyFeatureTest

If we pass the argument --unit, it will create a unit test instead.

php artisan make:test MyUnitTest --unit

Writting a test

This is an example of a test written using Pest.

test('example', function () {
  expect(false)->toBeTrue();
});

Pest also includes the describe() function for grouping related tests and the it() function for prefixing the test description, making our tests and their outputs more readable.

describe('MyClass', function () {
  it('should do the thing', function () {
    expect(MyClass::doTheThing())
      ->toBeString()
      ->toBe('the_thing');
  });
});

We can check the full list of Pest expectations by visiting its official documentation.

Pest hooks can be used for setting up a test or a test suite. The available methods are beforeEach(), afterEach(), beforeAll() and afterAll().

describe('SomeModel', function () {
  beforeEach(function () {
    $this->someRepository = new SomeRepository();
  });

  it('should be created', function () {
    $someModel = $this->someRepository->create();

    expect($someModel)->toBeInstanceOf(SomeModel::class);
  });

  afterEach(function () {
    $this->someRepository->reset();
  });
});

Using test doubles

Test doubles allow us to replace real implementations of the dependencies of a class in order to isolate its interactions. They are useful for unit tests or even integration tests when you do not want to work with the full chain of classes that interact in the whole process.

Mockery is the mocking library recommended by Pest.

We can install it using composer.

composer require mockery/mockery --dev

This is how we write a stub for a test.

describe('SubscribedUsersCounter', () => {
  it('should count the number of subscribed users', () => {
    $subscribedUserStub = Mockery::mock(User::class);
    $subscribedUserStub->shouldReceive('isSubscribed')->andReturn(true);
    $subscribedUsersCounter = new SubscribedUsersCounter();
    
    expect($subscribedUsersCounter->count([ $subscribedUserStub ]))
      ->toBe(1);
  });
});

This is how we write a spy for a test.

describe('Order', () => {
  it('should notify when it is processed', () => {
    $orderProcessedNotificationSpy = Mockery::spy(OrderProcessedNotification::class);
    $order = new Order();

    $order->process();

    $orderProcessedNotificationSpy
      ->shouldHaveReceived('toMail')
      ->once();
  });
});

Running tests

We can use the next artisan command to run our tests.

php artisan test

We can specify the test suite that we want to run or if we want to stop the execution when a test fails.

php artisan test --testsuite=Feature --stop-on-failure

One of the greatest options is to run tests in parallel instead of running them sequentially, which is the default option. This will greatly speed up the execution.

php artisan test --parallel

We can use the --coverage option in order to know how much of our application code is covered by our tests.

php artisan test --coverage

Passing the --min option will make the test suite fail if the threshold is not met.

php artisan test --coverage --min=95.0

Passing the --profile option will show us a list with our ten slowest tests so we can try to improve their performance.

php artisan test --profile
2024-05-23
Written by Samuel de Vega.
Tags