In previous post, we look at the benefits of visual snapshot testing for lazy people. How bare input/output code in a single file makes tests easy to read for new contributors.
Today, we look at how to maintain visual snapshot tests.
Let's say we need to add declare(strict_types=1);
to output part of 100 test fixtures? Would you add it manually in every single file?
Short quiz from last week: what is the visual snapshot test?
A test where the new test case is a single fixture file:
before
-----
after
Let's say we test a service that multiplies the input number by 5.
How would the fixture look like?
10
-----
50
Correct! Now let's learn something new.
"It's easy to write tests that are hard to maintain."
I'm currently working on a tool that migrates YAML configs to PHP. It's almost finished... but there is one thing missing in all those configs. PHP configs.
I forgot to add the declare(strict_types=1);
line. So now, every time you generate a PHP config, you have to run coding standards too on these files. So much extra work you, end-developers.
When was my mission changed to adding developers extra tedious work? We need to handle it.
parameters:
key: 'value'
-----
<?php
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$parameters->set('key', 'value');
};
If we are lucky and the pattern is unique, we can use PHPStorm find/replace or even regular expressions. This might work for this simple case, but soon fails for real-life cases like "add extra method call under each $service->set()".
We can do better.
With visual snapshot tests this is piece of cake. All we need is UPDATE_TESTS=1
env and normal PHPUnit run:
Now, all the 100 files have completed declare(strict_types=1);
:
parameters:
key: 'value'
-----
<?php
declare(strict_types=1);
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$parameters->set('key', 'value');
};
In the previous post, we looked on how to split and test fixture files.
We only update this code with a single method, that will handle the fixture updates:
<?php
use PHPUnit\Framework\TestCase;
final class FirstTryTest extends TestCase
{
public function test(): void
{
$filePath = __DIR__ . '/fixture/first_try.php';
$fixtureContent = file_get_contents($filePath);
[$input, $expectedOutput] = explode("\n-----\n", $fixtureContent);
// test your main domain service
$output = $this->processInputInYourDomain($input);
+ $this->updateFixture($input, $output, $filePath);
$this->assertSame($expectedOutput, $output);
}
}
And add the updateFixture()
method:
private function updateFixture(
string $input,
string $currentOutput,
string $fixtureFilePath
): void {
// only runs when UPDATE_TESTS=1 is put before PHPUnit run
if (! getenv('UPDATE_TESTS')) {
return;
}
// update changed output content part
$newOriginalContent = $input . PHP_EOL .
'-----' . PHP_EOL .
$currentOutput . PHP_EOL;
// update the fixture file
file_put_contents($fixtureFilePath, $newOriginalContent);
}
And that's it!
The best place to add updateFixture()
is an abstract test case, e.g., AbstractVisualSnapshotTestCase
. So we have one place to change.
Now you can do massive changes in your business logic, and even you rewrite the output completely, all you need to run is:
UPDATE_TESTS=1 vendor/bin/phpunit
Now we know the simplest way to maintain tests that are easy to read there is... or is it?
Happy coding!