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):
{
"$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.
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.
<?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);
}
}