Skip to content

Commit c29aa2c

Browse files
committed
Snapshot testing
1 parent b441453 commit c29aa2c

11 files changed

+259
-0
lines changed

src/Framework/Environment.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ class Environment
2727
/** Thread number when run tests in multi threads */
2828
public const THREAD = 'NETTE_TESTER_THREAD';
2929

30+
/** Should Tester update snapshots? */
31+
public const UPDATE_SNAPSHOTS = 'NETTE_TESTER_UPDATE_SNAPSHOTS';
32+
3033
/** @var bool used for debugging Tester itself */
3134
public static $debugMode = true;
3235

@@ -108,6 +111,11 @@ public static function setupErrors(): void
108111
self::removeOutputBuffers();
109112
echo "\nFatal error: $error[message] in $error[file] on line $error[line]\n";
110113
}
114+
} elseif (getenv(self::UPDATE_SNAPSHOTS) && Snapshot::$updatedSnapshots) {
115+
self::removeOutputBuffers();
116+
echo "\nThe following snapshots were updated, please make sure they are correct:\n"
117+
. implode("\n", Snapshot::$updatedSnapshots) . "\n";
118+
exit(Runner\Job::CODE_FAIL);
111119
} elseif (self::$checkAssertions && !Assert::$counter) {
112120
self::removeOutputBuffers();
113121
echo "\nError: This test forgets to execute an assertion.\n";

src/Framework/Snapshot.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Nette Tester.
5+
* Copyright (c) 2009 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Tester;
11+
12+
13+
/**
14+
* Snapshot testing helper.
15+
*/
16+
class Snapshot
17+
{
18+
public static $snapshotDir = 'snapshots';
19+
20+
public static $updatedSnapshots = [];
21+
22+
23+
/**
24+
* Compares value with a previously created snapshot.
25+
*/
26+
public static function match($value, string $snapshotName): void
27+
{
28+
$updateSnapshots = (bool) getenv(Environment::UPDATE_SNAPSHOTS);
29+
30+
$testFile = $_SERVER['argv'][0];
31+
$snapshotFile = self::getSnapshotFile($testFile, $snapshotName);
32+
33+
if (!file_exists($snapshotFile)) {
34+
if (!$updateSnapshots) {
35+
Assert::fail("Missing snapshot file '$snapshotFile', use --update-snapshots option to generate it.");
36+
}
37+
38+
self::write($snapshotFile, $value);
39+
}
40+
41+
$snapshot = self::read($snapshotFile);
42+
43+
try {
44+
Assert::equal($snapshot, $value, "Snapshot $snapshotName");
45+
46+
} catch (AssertException $e) {
47+
if (!$updateSnapshots) {
48+
throw $e;
49+
}
50+
51+
self::write($snapshotFile, $value);
52+
}
53+
}
54+
55+
56+
private static function getSnapshotFile(string $testFile, string $snapshotName): string
57+
{
58+
$path = self::$snapshotDir . DIRECTORY_SEPARATOR . pathinfo($testFile, PATHINFO_FILENAME) . '.' . $snapshotName . '.phps';
59+
if (!preg_match('#/|\w:#A', self::$snapshotDir)) {
60+
$path = dirname($testFile) . DIRECTORY_SEPARATOR . $path;
61+
}
62+
return $path;
63+
}
64+
65+
66+
private static function read(string $snapshotFile)
67+
{
68+
$snapshotContents = @file_get_contents($snapshotFile);
69+
if ($snapshotContents === false) {
70+
throw new \Exception("Unable to read snapshot file '$snapshotFile'.");
71+
}
72+
73+
return eval(substr($snapshotContents, strlen('<?php ')));
74+
}
75+
76+
77+
private static function write(string $snapshotFile, $value): void
78+
{
79+
$snapshotDirectory = dirname($snapshotFile);
80+
if (!is_dir($snapshotDirectory) && !mkdir($snapshotDirectory)) {
81+
throw new \Exception("Unable to create snapshot directory '$snapshotDirectory'.");
82+
}
83+
84+
$snapshotContents = '<?php return ' . var_export($value, true) . ';' . PHP_EOL;
85+
if (file_put_contents($snapshotFile, $snapshotContents) === false) {
86+
throw new \Exception("Unable to write snapshot file '$snapshotFile'.");
87+
}
88+
89+
self::$updatedSnapshots[] = $snapshotFile;
90+
}
91+
}

src/Runner/CliTester.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ public function run(): ?int
6565
if (isset($coverageFile)) {
6666
$runner->setEnvironmentVariable(Environment::COVERAGE, $coverageFile);
6767
}
68+
if ($this->options['--update-snapshots']) {
69+
$runner->setEnvironmentVariable(Environment::UPDATE_SNAPSHOTS, '1');
70+
}
6871

6972
if ($this->options['-o'] !== null) {
7073
ob_clean();
@@ -117,6 +120,7 @@ private function loadOptions(): CommandLine
117120
--colors [1|0] Enable or disable colors.
118121
--coverage <path> Generate code coverage report to file.
119122
--coverage-src <path> Path to source code.
123+
--update-snapshots Create or update snapshot files. Tests with snapshot changes will be marked as skipped.
120124
-h | --help This help.
121125

122126
XX

src/bootstrap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
require __DIR__ . '/Framework/TestCase.php';
1717
require __DIR__ . '/Framework/DomQuery.php';
1818
require __DIR__ . '/Framework/FileMutator.php';
19+
require __DIR__ . '/Framework/Snapshot.php';
1920
require __DIR__ . '/CodeCoverage/Collector.php';
2021
require __DIR__ . '/Runner/Job.php';
2122

tests/Framework/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
snapshots/

tests/Framework/Snapshot.match.phpt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Tester\Assert;
6+
use Tester\AssertException;
7+
use Tester\Snapshot;
8+
9+
require __DIR__ . '/../bootstrap.php';
10+
11+
Snapshot::$snapshotDir = __DIR__ . '/fixtures';
12+
13+
Snapshot::match(['answer' => 42], 'existingSnapshot');
14+
15+
Assert::exception(function () {
16+
Snapshot::match(['answer' => 43], 'existingSnapshot');
17+
}, AssertException::class, "Snapshot existingSnapshot: %a% should be equal to %a%");
18+
19+
Assert::exception(function () {
20+
Snapshot::match('value', 'nonExistingSnapshot');
21+
}, AssertException::class, "Missing snapshot file '%A%', use --update-snapshots option to generate it.");

tests/Framework/Snapshot.update.phpt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Tester\Assert;
6+
use Tester\Environment;
7+
use Tester\Helpers;
8+
use Tester\Snapshot;
9+
10+
require __DIR__ . '/../bootstrap.php';
11+
12+
putenv(Environment::UPDATE_SNAPSHOTS . '=1');
13+
Snapshot::$snapshotDir = __DIR__ . '/snapshots';
14+
Helpers::purge(Snapshot::$snapshotDir);
15+
16+
// newly created
17+
18+
Assert::false(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.newSnapshot.phps'));
19+
Snapshot::match(['answer' => 42], 'newSnapshot');
20+
Assert::true(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.newSnapshot.phps'));
21+
Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Snapshot.update.newSnapshot.phps'));
22+
23+
// existing
24+
25+
file_put_contents(
26+
Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps',
27+
'<?php return array(\'answer\' => 43);' . PHP_EOL
28+
);
29+
30+
Assert::true(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps'));
31+
Snapshot::match(['answer' => 42], 'updatedSnapshot');
32+
Assert::true(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps'));
33+
Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps'));
34+
35+
// Snapshot::$updatedSnapshots
36+
37+
Assert::equal([
38+
Snapshot::$snapshotDir . '/Snapshot.update.newSnapshot.phps',
39+
Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps',
40+
], Snapshot::$updatedSnapshots);
41+
42+
// reset the env variable so that the test does not fail due to updated snapshots
43+
putenv(Environment::UPDATE_SNAPSHOTS . '=0');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<?php return ['answer' => 42];

tests/Runner/Runner.snapshots.phpt

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Tester\Assert;
6+
use Tester\Dumper;
7+
use Tester\Runner\Test;
8+
9+
require __DIR__ . '/../bootstrap.php';
10+
require __DIR__ . '/../../src/Runner/OutputHandler.php';
11+
require __DIR__ . '/../../src/Runner/Test.php';
12+
require __DIR__ . '/../../src/Runner/TestHandler.php';
13+
require __DIR__ . '/../../src/Runner/Runner.php';
14+
15+
16+
class Logger implements Tester\Runner\OutputHandler
17+
{
18+
public $results = [];
19+
20+
21+
public function prepare(Test $test): void
22+
{
23+
}
24+
25+
26+
public function finish(Test $test): void
27+
{
28+
$this->results[basename($test->getFile())] = [$test->getResult(), $test->message];
29+
}
30+
31+
32+
public function begin(): void
33+
{
34+
}
35+
36+
37+
public function end(): void
38+
{
39+
}
40+
}
41+
42+
43+
Tester\Helpers::purge(__DIR__ . '/snapshots/snapshots');
44+
45+
46+
// first run, without update -> fail
47+
48+
$runner = new Tester\Runner\Runner(createInterpreter());
49+
$runner->paths[] = __DIR__ . '/snapshots/*.phptx';
50+
$runner->outputHandlers[] = $logger = new Logger;
51+
$runner->run();
52+
53+
Assert::same(Test::FAILED, $logger->results['update-snapshots.phptx'][0]);
54+
Assert::match(
55+
"Failed: Missing snapshot file '%a%', use --update-snapshots option to generate it.\n%A%",
56+
trim(Dumper::removeColors($logger->results['update-snapshots.phptx'][1]))
57+
);
58+
59+
// second run, with update -> skipped
60+
61+
$runner = new Tester\Runner\Runner(createInterpreter());
62+
$runner->paths[] = __DIR__ . '/snapshots/*.phptx';
63+
$runner->outputHandlers[] = $logger = new Logger;
64+
$runner->setEnvironmentVariable(Tester\Environment::UPDATE_SNAPSHOTS, '1');
65+
$runner->run();
66+
67+
Assert::same(Test::FAILED, $logger->results['update-snapshots.phptx'][0]);
68+
Assert::match(
69+
"The following snapshots were updated, please make sure they are correct:\n%a%.snapshot.phps",
70+
trim(Dumper::removeColors($logger->results['update-snapshots.phptx'][1]))
71+
);
72+
73+
// third run, without update -> pass
74+
75+
$runner = new Tester\Runner\Runner(createInterpreter());
76+
$runner->paths[] = __DIR__ . '/snapshots/*.phptx';
77+
$runner->outputHandlers[] = $logger = new Logger;
78+
$runner->run();
79+
80+
Assert::same(Test::PASSED, $logger->results['update-snapshots.phptx'][0]);

tests/Runner/snapshots/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
snapshots/
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
use Tester\Snapshot;
4+
5+
require __DIR__ . '/../../bootstrap.php';
6+
7+
Snapshot::$snapshotDir = __DIR__ . '/snapshots';
8+
Snapshot::match('snapshot value', 'snapshot');

0 commit comments

Comments
 (0)