-
Notifications
You must be signed in to change notification settings - Fork 630
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
feat(testing): Implement "assertSnapshot" #2039
Changes from 5 commits
73467da
4d8bf6c
4833857
31abdb8
418cc43
58df39e
f191dd3
9acd82c
ea2479f
2b717b6
46b0b9a
a199814
d36765c
9df0184
2538887
c710140
c53bb73
140bd96
b6d55e3
d024982
da8e582
03b250e
f4b4174
063261b
a4bf357
6ca3bd1
5ec8e41
2f0cc55
9e4390c
fe87cfa
b1782f8
21b96d0
7ad5a8b
666f32d
3902ec7
e13af8a
158b8d5
c3264dd
e1155a0
fddf34d
24f1868
9c34b02
03d2a4c
0a66471
4542ea9
faeb691
26aba27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -12,6 +12,8 @@ import { | |||||
stripColor, | ||||||
white, | ||||||
} from "../fmt/colors.ts"; | ||||||
import { fromFileUrl, parse, join } from "../path/mod.ts"; | ||||||
import { ensureFile, ensureFileSync } from "../fs/mod.ts"; | ||||||
import { diff, DiffResult, diffstr, DiffType } from "./_diff.ts"; | ||||||
|
||||||
const CAN_NOT_DISPLAY = "[Cannot display]"; | ||||||
|
@@ -864,3 +866,59 @@ export function unimplemented(msg?: string): never { | |||||
export function unreachable(): never { | ||||||
throw new AssertionError("unreachable"); | ||||||
} | ||||||
|
||||||
let snapshotFile: Record<string, unknown> | undefined = undefined; | ||||||
const updatedSnapshotFile: Record<string, unknown> = {}; | ||||||
// const snapshotMap: Record<string, number> = {}; | ||||||
|
||||||
export async function assertSnapshot(context: Deno.TestContext, actual: unknown) { | ||||||
const name = getName(context); | ||||||
// const count = getCount(name); | ||||||
|
||||||
if (!snapshotFile) { | ||||||
const snapshotPath = getSnapshotPath(); | ||||||
await ensureFile(snapshotPath); | ||||||
const file = await Deno.readTextFile(snapshotPath); | ||||||
snapshotFile = file ? JSON.parse(file) : {}; | ||||||
} | ||||||
const snapshot = snapshotFile?.[name]; | ||||||
if (!Deno.args.includes('--update')) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Could do something like this to support There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will handle this after denoland/deno#14007 merged! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW to pass There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. I also noticed that the flag for updating snapshot can affect other codes that wanted to test.. but I think that using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imo this is the best solution for first pass. But if/when snapshot testing is a more established then we should look to implement this on the Deno CLI instead; as @hyp3rflow mentioned, this can affect test execution. |
||||||
assertEquals(actual, snapshot); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation will error on: const actual = {
fn: () => { /* ... */ }
} As snapshots need to be represented as strings anyway, I would argue it makes more sense to use the {
"Snapshot Test > babo > merong": [
"{",
" b: 2,",
" c: 3,",
"}"
]
} This would enable us to catch a much wider range of errors in our snapshots. E.g. the following would error on // First run
const actual = {
testClass: class Test {}
}
// --snip--
assertSnapshot(test, actual)
// Second run
const actual = {
testClass: class NotTest {}
}
// --snip--
assertSnapshot(test, actual) But would not error (or worse, always error) if we only store the JSON representation. Additionally, this representation enables us to use the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think Deno.inspect() can be a good choice for serialization method. What do you think? @bcheidemann There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this would definitely do the job. I would suggest to use the |
||||||
} else { | ||||||
let isEqual = true; | ||||||
try { | ||||||
assertEquals(actual, snapshot); | ||||||
} catch { | ||||||
isEqual = false; | ||||||
} | ||||||
if (!isEqual) console.info("Snapshot updated", name); | ||||||
} | ||||||
updatedSnapshotFile[name] = actual; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this mean this implementation supports only 1 snapshot per test case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the test case mean one for example, if I test Deno.test("Snapshot Test A", async (t) => {
await assertSnapshot(t, { a: 1, b: 2 });
await t.step("yaho", async (t) => {
await assertSnapshot(t, { b: 2, c: 3 });
await t.step("haha", async (t) => {
await assertSnapshot(t, { b: 2, c: 4 });
});
});
});
Deno.test("Snapshot Test A2", async (t) => {
await assertSnapshot(t, { a: 1, b: 2 });
await t.step("what", async (t) => {
await assertSnapshot(t, { b: 2, c: 3 });
await t.step("good", async (t) => {
await assertSnapshot(t, { b: 2, c: 4 });
});
});
}); the output, {
"Snapshot Test A": {
"a": 1,
"b": 2
},
"Snapshot Test A > yaho": {
"b": 2,
"c": 3
},
"Snapshot Test A > yaho > haha": {
"b": 2,
"c": 4
},
"Snapshot Test A2": {
"a": 1,
"b": 2
},
"Snapshot Test A2 > what": {
"b": 2,
"c": 3
},
"Snapshot Test A2 > what > good": {
"b": 2,
"c": 4
}
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about the below case? I think the second Deno.test("Snapshot Test A", async (t) => {
await assertSnapshot(t, { a: 1, b: 2 });
await assertSnapshot(t, { c: 3, d: 4 });
}); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aha! Sorry, now I know what you mean. That case will be handled with commented count. (Currently not implemented perfectly so I commented) @kt3k There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, ok. I see. |
||||||
|
||||||
globalThis.onunload = writeSnapshotFileSync; | ||||||
|
||||||
// function getCount() { | ||||||
// const count = snapshotMap?.[name] ? snapshotMap[name] : 1; | ||||||
// snapshotMap[name] = count + 1; | ||||||
// return count; | ||||||
// } | ||||||
|
||||||
function getName(context: Deno.TestContext): string { | ||||||
if (context.parent) return `${getName(context.parent)} > ${context.name}`; | ||||||
return context.name; | ||||||
} | ||||||
|
||||||
function getSnapshotPath() { | ||||||
const testFile = fromFileUrl(context.origin); | ||||||
const parts = parse(testFile); | ||||||
return `${join(parts.dir, parts.name)}.snap`; | ||||||
} | ||||||
|
||||||
function writeSnapshotFileSync() { | ||||||
const snapshotPath = getSnapshotPath(); | ||||||
ensureFileSync(snapshotPath); | ||||||
const file = JSON.stringify(updatedSnapshotFile, null, 2); | ||||||
Deno.writeTextFileSync(snapshotPath, file); | ||||||
console.log('Snapshot updated!'); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Maybe we could mimic Jest here and log how many snapshots were updated? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{ | ||
"Snapshot Test": { | ||
"a": 1, | ||
"b": 2 | ||
}, | ||
"Snapshot Test > babo": { | ||
"b": 2, | ||
"c": 3 | ||
}, | ||
"Snapshot Test > babo > merong": { | ||
"b": 2, | ||
"c": 4 | ||
} | ||
} |
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 suggest we also expose a function called
assertSnapshotSync
.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.
Good! I think we can implement that after denoland/deno#14007 merged.
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.
It's hard to do this with the way we now read the snapshot file. I think this can probably be implemented separately if it's needed