-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Replace jest-snapshot #1223
Conversation
Hi, Let me know if I can further help, |
Hey @lusentis, I don't think it can be considered a bug, since t.deepEqual({a: 1, b: undefined}, {a: 1}); |
Sure, I didn't explain it very well... This code: const obj = {
a: 123,
b: 'bar',
c: null,
d: undefined
}
t.snapshot(obj); produces this snapshot when running {
"example snapshot of an object with undefined values": {
"a": 123,
"b": "bar",
"c": null
}
} which is missing the "d" key from the Object. |
Right, it is an unexpected behavior for sure. This is happening, because we use |
@lusentis Thanks for trying out this PR and reporting the issue! |
A quick fix would be to |
Found this: https://github.com/kaelzhang/node-code-stringify it might help (I'm playing with this: https://github.com/lusentis/ava/commit/a39f1be2a48aa7c7e3b2f9a2aa6958a46b2e302b). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think at this stage we should be a little more opinionated (and conservative) in what snapshot testing supports. Notably, using JSON for serialization means:
undefined
,Symbol()
and function property values are droppedundefined
,Symbol()
and function array items are replaced bynull
- regular expression property values and array items are replaced by
{}
- no support for built-ins such as
Map
andSet
- if the input value utilizes
toJSON()
functions then the serialized value will definitely not match
I'm OK with that actually, though we could refine in the future.
What we should do, however, is when t.snapshot()
is first called (and there is no saved snapshot), is to serialize and deserialize the actual value to make sure it behaves the same as it would on the next run (when there is a saved snapshot).
We should also document these caveats and perhaps link to that documentation when displaying the difference.
this.options = options || {}; | ||
|
||
if (fs.existsSync(this.filePath)) { | ||
this.tests = JSON.parse(fs.readFileSync(this.filePath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd just read the file and ignore any errors:
let contents
try {
contents = fs.readFileSync(this.filePath);
} catch (err) {}
this.tests = contents ? JSON.parse(contents) : {}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
|
||
const x = module.exports = Snapshot; | ||
|
||
Snapshot.getSnapshot = () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why export the Snapshot
class at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. Perhaps export both Snapshot
and getSnapshot
separately (rather than exporting the class as the main?)
Alternatively you could use proxyquire
and inject the globals
dependency, though you'd have to reload for each test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps export both Snapshot and getSnapshot separately (rather than exporting the class as the main?)
This doesn't seem to change anything. What is the benefit of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it better separates the "public" API (getSnapshot()
) from the truly internal API (Snapshot
constructor) which is needed for tests only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, but the thing is, both getSnapshot()
and Snapshot
aren't public. t.snapshot
is an access point for these, users won't ever need to access the above mentioned stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know. Just voicing my preference 😄
|
||
Snapshot.getSnapshot = () => { | ||
if (!x.snapshot) { | ||
x.snapshot = new Snapshot(globals.options.file, {update: globals.options.updateSnapshots}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you document that this is only loaded in a worker process, so we only need one snapshot instance for the file that worker is testing?
@novemberborn Assuming it serialise the new value and deserialise it before comparing it to the deserialised old value, it can support toJSON, no? |
ps: when storing a new value, snapshot could warn about any value lost in the process: it could show a diff of the value with the deserialised stored value. |
@novemberborn For me as a user of ava, I'd expect the same level of robustness as I get from jest snapshots. Also when you've listed the features not supported by raw json snapshots, you failed to mention Type support: https://www.npmjs.com/package/pretty-format#type-support |
@capaj yea that's a good idea. @vadimdemedes if we go this route we'd have to do string comparisons on the formatted values. Can we do string diffs in case the new value doesn't match the saved snapshot? |
throw new TypeError('Test file path is required'); | ||
} | ||
|
||
this.dirPath = path.join(path.dirname(testPath), '__snapshots__'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Jest convention aside, is there a reason we should stick to this format? Our naming scheme is helpers
and fixtures
, not __fixtures__
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, I'd prefer snapshots
too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be configurable though? Folks who follow a __tests__
scheme will want to use __snapshots__
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would really prefer not introducing more options. Can we make it __snapshots__
if inside a __tests__
directory, otherwise snapshots
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would really prefer not introducing more options. Can we make it snapshots if inside a tests directory, otherwise snapshots?
Yea that sounds great to me.
We could use pretty-format to serialize values into snapshots, but then we wouldn't get a highlighted diff (when t.snapshot fails). I think it'd look weird, if only t.snapshot didn't support diff highlighting, while other assertions did. |
throw new TypeError('Test file path is required'); | ||
} | ||
|
||
this.dirPath = path.join(path.dirname(testPath), '__snapshots__'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
this.options = options || {}; | ||
|
||
if (fs.existsSync(this.filePath)) { | ||
this.tests = JSON.parse(fs.readFileSync(this.filePath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
|
||
save() { | ||
mkdirp.sync(this.dirPath); | ||
fs.writeFileSync(this.filePath, JSON.stringify(this.tests, null, ' ')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
' '
=> '\t'
I added a couple of todo items to the PR description. |
I just realized the benefit of doing like Jest is with a JS snapshot file is that they can have template literals with multiple line, so they can have good git diffing when updating snapshots. |
@sindresorhus what are you intending here? I'm under the impression that if all tests are run, the snapshots will be updated correctly. If you use |
I've also added a few TODO items. Let's decide our direction first though: #1275. |
Not just removing a test file. Imagine the user removes a single test. There will now be a stale entry in the snapshot file. I would expect AVA to remove the stale test entry in the snapshot when I run This is made harder by
Yes |
Yes, I'd imagine it would do that. That's easy to do though. Removing a snapshot file after a test file has been removed / renamed isn't too hard either since we glob all test files and can compare that to the existing snapshot files.
I see. As you said, we can simply not clean up snapshots for skipped tests. |
One more item for the TODO:
Currently, the snapshots are indexed by test name, it seems; however, when I tested in my project, I received false positives when there were multiple snapshots in a test. The current implementation in master (using For example, if my test is called What I'm currently doing is a bit nicer to read (diff-wise), which is the snapshot a hash with the different snapshots. That way they each have a name associated with them. |
} | ||
|
||
save() { | ||
mkdirp.sync(this.dirPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add something like:
let hasTests = false;
for(var key in obj) {
if(obj.hasOwnProperty(key))
hasTests = true;
break;
}
}
if (!hasTests) return;
at the beginning of this function. That way we don't end up making a bunch of empty files/divs.
We also need to ask ourselves what we want to do if someone deletes a test. Should we delete the snapshots too? Or leave them there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, I forked off and tried this out and it works quite well: TzviPM@c95962a
On my project, the diffs seem to be backwards. For example, if my test does: Object {
- foo: foo,
+ bar: bar,
} but I see the opposite: Object {
- bar: bar,
+ foo: foo,
} |
I tried playing around with non-native objects and it became quite messy quite fast. Perhaps the MVP doesn't need to support anything that can't be serialized / |
Humbly, I think this is superseded by #1341 (comment) 😄 |
Superseded by #1341. |
After investigating #1218, I realized jest-snapshot is too much for us and I believe we can bundle a simpler solution. We cut down on unnecessary dependencies and lay more groundwork for universal (React/Preact/Inferno/more) assertions by not making assumptions about the content of your snapshots.
Since our replacement is basically just born, please report any bugs or missing/unexpected behavior.
If this PR lands and if you've used
t.snapshot
already, make sure to update them using--update-snapshots
flag.Todo:
--update-snapshots
flag #1218 (comment)__snapshots__
if inside a__tests__
directory, otherwisesnapshots
(added by @novemberborn)--update-snapshots
flag #1218 (comment) (added by @novemberborn)