Skip to content

Commit b7aee56

Browse files
Merge pull request #35 from SjorsO/master
add file snapshot assertion
2 parents e551d16 + bc7529d commit b7aee56

18 files changed

+289
-1
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,12 @@ composer require spatie/phpunit-snapshot-assertions
7878

7979
## Usage
8080

81-
To make snapshot assertions, use the `Spatie\Snapshots\MatchesSnapshots` trait in your test case class. This adds four assertion methods to the class:
81+
To make snapshot assertions, use the `Spatie\Snapshots\MatchesSnapshots` trait in your test case class. This adds five assertion methods to the class:
8282

8383
- `assertMatchesSnapshot($actual)`
8484
- `assertMatchesJsonSnapshot($actual)`
8585
- `assertMatchesXmlSnapshot($actual)`
86+
- `assertMatchesFileSnapshot($filePath)`
8687
- `assertMatchesFileHashSnapshot($filePath)`
8788

8889
### Snapshot Testing 101
@@ -159,6 +160,14 @@ As a result, our snapshot file returns "bar" instead of "foo".
159160
<?php return 'bar';
160161
```
161162

163+
### File snapshots
164+
165+
The `MatchesSnapshots` trait offers two ways to assert that a file is identical to the snapshot that was made the first time the test was run:
166+
167+
The `assertMatchesFileHashSnapshot($filePath)` assertion asserts that the hash of the file passed into the function and the hash saved in the snapshot match. This assertion is fast and uses very little disk space. The downside of this assertion is that there is no easy way to see how the two files differ if the test fails.
168+
169+
The `assertMatchesFileSnapshot($filePath)` assertion works almost the same way as the file hash assertion, except that it actually saves the whole file in the snapshots directory. If the assertion fails, it places the failed file next to the snapshot file so they can easily be manually compared. The persisted failed file is automatically deleted when the test passes. This assertion is most useful when working with binary files that should be manually compared like images or pdfs.
170+
162171
### Customizing Snapshot Ids and Directories
163172

164173
Snapshot ids are generated via the `getSnapshotId` method on the `MatchesSnapshot` trait. Override the method to customize the id. By default, a snapshot id exists of the test name, the test case name and an incrementing value, e.g. `Test__my_test_case__1`.

src/Filesystem.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,37 @@ public function has(string $filename): bool
2727
return file_exists($this->path($filename));
2828
}
2929

30+
/**
31+
* Get all file names in this directory that have the same name
32+
* as $fileName, but have a different file extension.
33+
*
34+
* @param string $fileName
35+
*
36+
* @return array
37+
*/
38+
public function getNamesWithDifferentExtension(string $fileName)
39+
{
40+
if (! file_exists($this->basePath)) {
41+
return [];
42+
}
43+
44+
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
45+
46+
$baseName = substr($fileName, 0, strlen($fileName) - strlen($extension) - 1);
47+
48+
$allNames = scandir($this->basePath);
49+
50+
$namesWithDifferentExtension = array_filter($allNames, function ($existingName) use ($baseName, $extension) {
51+
$existingExtension = pathinfo($existingName, PATHINFO_EXTENSION);
52+
53+
$existingBaseName = substr($existingName, 0, strlen($existingName) - strlen($existingExtension) - 1);
54+
55+
return $existingBaseName === $baseName && $existingExtension !== $extension;
56+
});
57+
58+
return array_values($namesWithDifferentExtension);
59+
}
60+
3061
public function read(string $filename): string
3162
{
3263
return file_get_contents($this->path($filename));
@@ -40,4 +71,23 @@ public function put(string $filename, string $contents)
4071

4172
file_put_contents($this->path($filename), $contents);
4273
}
74+
75+
public function delete(string $fileName)
76+
{
77+
return unlink($this->path($fileName));
78+
}
79+
80+
public function copy(string $filePath, string $fileName)
81+
{
82+
if (! file_exists($this->basePath)) {
83+
mkdir($this->basePath);
84+
}
85+
86+
copy($filePath, $this->path($fileName));
87+
}
88+
89+
public function fileEquals(string $filePath, string $fileName)
90+
{
91+
return sha1_file($filePath) === sha1_file($this->path($fileName));
92+
}
4393
}

src/MatchesSnapshots.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public function assertMatchesFileHashSnapshot($filePath)
4747
$this->assertMatchesSnapshot($actual);
4848
}
4949

50+
public function assertMatchesFileSnapshot($file)
51+
{
52+
$this->doFileSnapshotAssertion($file);
53+
}
54+
5055
/**
5156
* Determines the snapshot's id. By default, the test case's class and
5257
* method names are used.
@@ -74,6 +79,20 @@ protected function getSnapshotDirectory(): string
7479
'__snapshots__';
7580
}
7681

82+
/**
83+
* Determines the directory where file snapshots are stored. By default a
84+
* `__snapshots__/files` directory is created at the same level as the
85+
* test class.
86+
*
87+
* @return string
88+
*/
89+
protected function getFileSnapshotDirectory(): string
90+
{
91+
return $this->getSnapshotDirectory().
92+
DIRECTORY_SEPARATOR.
93+
'files';
94+
}
95+
7796
/**
7897
* Determines whether or not the snapshot should be updated instead of
7998
* matched.
@@ -124,6 +143,69 @@ protected function doSnapshotAssertion($actual, Driver $driver)
124143
}
125144
}
126145

146+
protected function doFileSnapshotAssertion(string $filePath)
147+
{
148+
if (! file_exists($filePath)) {
149+
$this->fail('File does not exist');
150+
}
151+
152+
$fileExtension = pathinfo($filePath, PATHINFO_EXTENSION);
153+
154+
if (empty($fileExtension)) {
155+
$this->fail("Unable to make a file snapshot, file does not have a file extension ({$filePath})");
156+
}
157+
158+
$fileSystem = Filesystem::inDirectory($this->getFileSnapshotDirectory());
159+
160+
$this->snapshotIncrementor++;
161+
162+
$snapshotId = $this->getSnapshotId().'.'.$fileExtension;
163+
164+
// If $filePath has a different file extension than the snapshot, the test should fail
165+
if ($namesWithDifferentExtension = $fileSystem->getNamesWithDifferentExtension($snapshotId)) {
166+
// There is always only one existing snapshot with a different extension
167+
$existingSnapshotId = $namesWithDifferentExtension[0];
168+
169+
if ($this->shouldUpdateSnapshots()) {
170+
$fileSystem->delete($existingSnapshotId);
171+
172+
$fileSystem->copy($filePath, $snapshotId);
173+
174+
return $this->markTestIncomplete("File snapshot updated for {$snapshotId}");
175+
}
176+
177+
$expectedExtension = pathinfo($existingSnapshotId, PATHINFO_EXTENSION);
178+
179+
return $this->fail("File did not match the snapshot file extension (expected: {$expectedExtension}, was: {$fileExtension})");
180+
}
181+
182+
$failedSnapshotId = $snapshotId.'_failed.'.$fileExtension;
183+
184+
if ($fileSystem->has($failedSnapshotId)) {
185+
$fileSystem->delete($failedSnapshotId);
186+
}
187+
188+
if (! $fileSystem->has($snapshotId)) {
189+
$fileSystem->copy($filePath, $snapshotId);
190+
191+
$this->markTestIncomplete("File snapshot created for {$snapshotId}");
192+
}
193+
194+
if (! $fileSystem->fileEquals($filePath, $snapshotId)) {
195+
if ($this->shouldUpdateSnapshots()) {
196+
$fileSystem->copy($filePath, $snapshotId);
197+
198+
$this->markTestIncomplete("File snapshot updated for {$snapshotId}");
199+
}
200+
201+
$fileSystem->copy($filePath, $failedSnapshotId);
202+
203+
$this->fail("File did not match snapshot ({$snapshotId})");
204+
}
205+
206+
$this->assertTrue(true);
207+
}
208+
127209
protected function createSnapshotAndMarkTestIncomplete(Snapshot $snapshot, $actual)
128210
{
129211
$snapshot->create($actual);

tests/Integration/AssertionTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ public function can_match_a_file_hash_snapshot()
4949
$this->assertMatchesFileHashSnapshot($filePath);
5050
}
5151

52+
/** @test */
53+
public function can_match_a_file_snapshot()
54+
{
55+
$filePath = __DIR__.'/stubs/test_files/friendly_man.jpg';
56+
57+
$this->assertMatchesFileSnapshot($filePath);
58+
}
59+
5260
/** @test */
5361
public function can_do_multiple_snapshot_assertions()
5462
{

tests/Integration/MatchesSnapshotTest.php

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ public function it_can_create_a_snapshot_from_json()
6868
);
6969
}
7070

71+
/** @test */
72+
public function it_can_create_a_snapshot_from_a_file()
73+
{
74+
$mockTrait = $this->getMatchesSnapshotMock();
75+
76+
$this->expectIncompleteMatchesSnapshotTest($mockTrait);
77+
78+
$mockTrait->assertMatchesFileSnapshot(__DIR__.'/stubs/test_files/friendly_man.jpg');
79+
80+
$this->assertSnapshotMatchesExample(
81+
'files/MatchesSnapshotTest__it_can_create_a_snapshot_from_a_file__1.jpg',
82+
'file.jpg'
83+
);
84+
}
85+
7186
/** @test */
7287
public function it_can_match_an_existing_string_snapshot()
7388
{
@@ -140,6 +155,75 @@ public function it_can_mismatch_a_file_hash_snapshot()
140155
$mockTrait->assertMatchesFileHashSnapshot(__DIR__.'/stubs/example_snapshots/snapshot.json');
141156
}
142157

158+
/** @test */
159+
public function it_can_mismatch_a_file_snapshot()
160+
{
161+
$mockTrait = $this->getMatchesSnapshotMock();
162+
163+
$this->expectFail($mockTrait);
164+
165+
$mockTrait->assertMatchesFileSnapshot(__DIR__.'/stubs/test_files/troubled_man.jpg');
166+
}
167+
168+
/** @test */
169+
public function it_can_mismatch_a_file_snapshot_with_a_different_extension()
170+
{
171+
$mockTrait = $this->getMatchesSnapshotMock();
172+
173+
$this->expectFail($mockTrait);
174+
175+
$mockTrait->assertMatchesFileSnapshot(__DIR__.'/stubs/test_files/no_man.png');
176+
}
177+
178+
/** @test */
179+
public function it_needs_a_file_extension_to_do_a_file_snapshot_assertion()
180+
{
181+
$mockTrait = $this->getMatchesSnapshotMock();
182+
183+
$this->expectFail($mockTrait);
184+
185+
$filePath = __DIR__.'/stubs/test_files/file_without_extension';
186+
187+
$this->assertFileExists($filePath);
188+
189+
$mockTrait->assertMatchesFileSnapshot($filePath);
190+
}
191+
192+
/** @test */
193+
public function it_persists_the_failed_file_after_mismatching_a_file_snapshot()
194+
{
195+
$mockTrait = $this->getMatchesSnapshotMock();
196+
197+
$this->expectFail($mockTrait);
198+
199+
$mismatchedFile = __DIR__.'/stubs/test_files/troubled_man.jpg';
200+
201+
$mockTrait->assertMatchesFileSnapshot($mismatchedFile);
202+
203+
$persistedFailedFile = __DIR__.'/__snapshots__/files/MatchesSnapshotTest__it_persists_the_failed_file_after_mismatching_a_file_snapshot__1.jpg_failed.jpg';
204+
205+
$this->assertFileExists($persistedFailedFile);
206+
$this->assertFileEquals($mismatchedFile, $persistedFailedFile);
207+
}
208+
209+
/** @test */
210+
public function it_deletes_the_persisted_failed_file_before_a_file_snapshot_assertion()
211+
{
212+
$mockTrait = $this->getMatchesSnapshotMock();
213+
214+
$mockTrait
215+
->expects($this->once())
216+
->method('assertTrue');
217+
218+
$persistedFailedFile = __DIR__.'/__snapshots__/files/MatchesSnapshotTest__it_deletes_the_persisted_failed_file_before_a_file_snapshot_assertion__1.jpg_failed.jpg';
219+
220+
$this->assertTrue(touch($persistedFailedFile));
221+
222+
$mockTrait->assertMatchesFileSnapshot(__DIR__.'/stubs/test_files/friendly_man.jpg');
223+
224+
$this->assertFileNotExists($persistedFailedFile);
225+
}
226+
143227
/** @test */
144228
public function it_can_update_a_string_snapshot()
145229
{
@@ -191,13 +275,60 @@ public function it_can_update_a_json_snapshot()
191275
);
192276
}
193277

278+
/** @test */
279+
public function it_can_update_a_file_snapshot()
280+
{
281+
$_SERVER['argv'][] = '--update-snapshots';
282+
283+
$mockTrait = $this->getMatchesSnapshotMock();
284+
285+
$this->expectIncompleteMatchesSnapshotTest($mockTrait);
286+
287+
$mockTrait->assertMatchesFileSnapshot(__DIR__.'/stubs/test_files/friendly_man.jpg');
288+
289+
$this->assertSnapshotMatchesExample(
290+
'files/MatchesSnapshotTest__it_can_update_a_file_snapshot__1.jpg',
291+
'file.jpg'
292+
);
293+
}
294+
295+
/** @test */
296+
public function it_can_update_a_file_snapshot_with_a_different_extension()
297+
{
298+
$_SERVER['argv'][] = '--update-snapshots';
299+
300+
$mockTrait = $this->getMatchesSnapshotMock();
301+
302+
$this->expectIncompleteMatchesSnapshotTest($mockTrait);
303+
304+
$oldSnapshot = __DIR__.'/__snapshots__/files/MatchesSnapshotTest__it_can_update_a_file_snapshot_with_a_different_extension__1.jpg';
305+
306+
$this->assertFileExists($oldSnapshot);
307+
308+
$mockTrait->assertMatchesFileSnapshot(__DIR__.'/stubs/test_files/no_man.png');
309+
310+
$this->assertSnapshotMatchesExample(
311+
'files/MatchesSnapshotTest__it_can_update_a_file_snapshot_with_a_different_extension__1.png',
312+
'file.png'
313+
);
314+
315+
$this->assertFileNotExists($oldSnapshot);
316+
}
317+
194318
private function expectIncompleteMatchesSnapshotTest(PHPUnit_Framework_MockObject_MockObject $matchesSnapshotMock)
195319
{
196320
$matchesSnapshotMock
197321
->expects($this->once())
198322
->method('markTestIncomplete');
199323
}
200324

325+
private function expectFail(PHPUnit_Framework_MockObject_MockObject $matchesSnapshotMock)
326+
{
327+
$matchesSnapshotMock
328+
->expects($this->once())
329+
->method('fail');
330+
}
331+
201332
private function expectFailedMatchesSnapshotTest()
202333
{
203334
if (class_exists('PHPUnit\Framework\ExpectationFailedException')) {
@@ -216,6 +347,9 @@ private function getMatchesSnapshotMock(): PHPUnit_Framework_MockObject_MockObje
216347
'markTestIncomplete',
217348
'getSnapshotId',
218349
'getSnapshotDirectory',
350+
'getFileSnapshotDirectory',
351+
'fail',
352+
'assertTrue',
219353
];
220354

221355
$matchesSnapshotMock = $this->getMockForTrait(
@@ -234,6 +368,11 @@ private function getMatchesSnapshotMock(): PHPUnit_Framework_MockObject_MockObje
234368
->method('getSnapshotDirectory')
235369
->willReturn(__DIR__.'/__snapshots__');
236370

371+
$matchesSnapshotMock
372+
->expects($this->any())
373+
->method('getFileSnapshotDirectory')
374+
->willReturn(__DIR__.'/__snapshots__/files');
375+
237376
return $matchesSnapshotMock;
238377
}
239378
}
7.69 KB
Loading
7.69 KB
Loading
7.69 KB
Loading
3.09 KB
Loading
3.09 KB
Loading

0 commit comments

Comments
 (0)