15. Mutation testing. Test your tests. Laravel specific test mutators

Video version
(Leave your feedback on YouTube)

Mutation Tests

Package for mutation testing in PHP infection.github.io

Laravel specific mutators (self-made) Important: I do not support them and am not responsible for the code style. Make it open source if you want.

Mutation tests literally change the code. Each change is called a mutator. For each change caused by each mutator, all tests are run. If the tests fail, they have not covered that part of the code.

Current infection configuration (will be updated):

infection.json5
{
  "$schema": "vendor/infection/infection/resources/schema.json",
  "source": {
    "directories": [
      "Modules",
      "app/Services",
      "app/Traits"
    ],
    "excludes": [
      "Tests",
      "Database"
    ]
  },
  "logs": {
    "text": "infection.log"
  },
  "mutators": {
    "@default": true,
    "PublicVisibility": {
      "ignoreSourceCodeByRegex": [
        "public function asCommand.*",
        "public function asListener.*",
        "public function asJob.*",
        "public static function boot.*",
        "public function scope.*"
      ]
    },
    "ProtectedVisibility": {
      "ignoreSourceCodeByRegex": [
        "protected static function newFactory.*",
        "protected function slugSource.*"
      ]
    },
    "MethodCallRemoval": {
      "ignoreSourceCodeByRegex": [
        ".*select.*"
      ]
    },
    "Mutator\\Colection\\First\\LaravelCollectionFirst": true,
    "Mutator\\Colection\\Last\\LaravelCollectionLast": true,
    "Mutator\\Colection\\FirstOrFail\\LaravelCollectionFirstOrFail": true,
    "Mutator\\Eloquent\\OrderBy\\LaravelEloquentOrderBy": true,
    "Mutator\\Eloquent\\OrderByDesc\\LaravelEloquentOrderByDesc": true,
    "Mutator\\Eloquent\\FindOrFail\\LaravelEloquentFindOrFail": true,
  }
}

Mutation tests heavily load the system. VERY heavily. Therefore, I run them only for code changes and only on developer devices (pre-push git hook). I do not recommend running them as a regular step in CI/CD.

.husky/pre-push
docker exec -t bh-app ./vendor/bin/infection  \
--git-diff-filter= \
--git-diff-base=main

Testing schedule events

I recommend testing if events are scheduled. Their absence can cause many issues in production. I did not find an out-of-the-box solution, so I wrote this. If you see any drawbacks, let me know.

Modules/HealthCheck/Tests/Unit/HealthCheckServiceProviderTest.php
<?php

namespace Modules\HealthCheck\Tests\Unit;

use Illuminate\Console\Scheduling\Event;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Arr;
use Tests\TestCase;

class HealthCheckServiceProviderTest extends TestCase
{
    /**
     * @test
     */
    public function canRegisterHealthHeartbeatScheduleEvents()
    {
        $scheduleEvents = app()->make(Schedule::class)->events();
        $command = Arr::first(
            $scheduleEvents,
            fn (Event $event) => str_contains($event->command, 'health:schedule-check-heartbeat') &&
            $event->expression === '* * * * *'
        );
        $this->assertNotNull($command);
    }

    /**
     * @test
     */
    public function canRegisterHealthPruneModelEvents()
    {
        $scheduleEvents = app()->make(Schedule::class)->events();
        $command = Arr::first(
            $scheduleEvents,
            fn (Event $event) => str_contains(
                $event->command,
                "model:prune --model='Spatie\Health\Models\HealthCheckResultHistoryItem'"
            ) &&
            $event->expression === '0 0 * * *'
        );
        $this->assertNotNull($command);
    }
}