Skip to content

Commit c366887

Browse files
authored
feat(testing): Add support for object assertion against object subset (denoland/deno#8001)
This commit add supports for a new assertion function "assertObjectMatch" which allows to test an actual object against an expected object subset (i.e. inclusivity, not equality).
1 parent a720931 commit c366887

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed

testing/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pretty-printed diff of failing assertion.
2525
`expected`.
2626
- `assertArrayContains()` - Make an assertion that `actual` array contains the
2727
`expected` values.
28+
- `assertObjectMatch()` - Make an assertion that `actual` object match
29+
`expected` subset object
2830
- `assertThrows()` - Expects the passed `fn` to throw. If `fn` does not throw,
2931
this function does. Also compares any errors thrown to an optional expected
3032
`Error` class and checks that the error `.message` includes an optional

testing/asserts.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,48 @@ export function assertNotMatch(
429429
}
430430
}
431431

432+
/**
433+
* Make an assertion that `actual` object is a subset of `expected` object, deeply.
434+
* If not, then throw.
435+
*/
436+
export function assertObjectMatch(
437+
actual: Record<PropertyKey, unknown>,
438+
expected: Record<PropertyKey, unknown>,
439+
): void {
440+
type loose = Record<PropertyKey, unknown>;
441+
const seen = new WeakMap();
442+
return assertEquals(
443+
(function filter(a: loose, b: loose): loose {
444+
// Prevent infinite loop with circular references with same filter
445+
if ((seen.has(a)) && (seen.get(a) === b)) {
446+
return a;
447+
}
448+
seen.set(a, b);
449+
// Filter keys and symbols which are present in both actual and expected
450+
const filtered = {} as loose;
451+
const entries = [
452+
...Object.getOwnPropertyNames(a),
453+
...Object.getOwnPropertySymbols(a),
454+
]
455+
.filter((key) => key in b)
456+
.map((key) => [key, a[key as string]]) as Array<[string, unknown]>;
457+
// Build filtered object and filter recursively on nested objects references
458+
for (const [key, value] of entries) {
459+
if (typeof value === "object") {
460+
const subset = (b as loose)[key];
461+
if ((typeof subset === "object") && (subset)) {
462+
filtered[key] = filter(value as loose, subset as loose);
463+
continue;
464+
}
465+
}
466+
filtered[key] = value;
467+
}
468+
return filtered;
469+
})(actual, expected),
470+
expected,
471+
);
472+
}
473+
432474
/**
433475
* Forcefully throws a failed assertion
434476
*/

testing/asserts_test.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
assertNotEquals,
1010
assertNotMatch,
1111
assertNotStrictEquals,
12+
assertObjectMatch,
1213
assertStrictEquals,
1314
assertStringContains,
1415
assertThrows,
@@ -259,6 +260,180 @@ Deno.test("testingAssertStringNotMatchingThrows", function (): void {
259260
assert(didThrow);
260261
});
261262

263+
Deno.test("testingAssertObjectMatching", function (): void {
264+
const sym = Symbol("foo");
265+
const a = { foo: true, bar: false };
266+
const b = { ...a, baz: a };
267+
const c = { ...b, qux: b };
268+
const d = { corge: c, grault: c };
269+
const e = { foo: true } as { [key: string]: unknown };
270+
e.bar = e;
271+
const f = { [sym]: true, bar: false };
272+
// Simple subset
273+
assertObjectMatch(a, {
274+
foo: true,
275+
});
276+
// Subset with another subset
277+
assertObjectMatch(b, {
278+
foo: true,
279+
baz: { bar: false },
280+
});
281+
// Subset with multiple subsets
282+
assertObjectMatch(c, {
283+
foo: true,
284+
baz: { bar: false },
285+
qux: {
286+
baz: { foo: true },
287+
},
288+
});
289+
// Subset with same object reference as subset
290+
assertObjectMatch(d, {
291+
corge: {
292+
foo: true,
293+
qux: { bar: false },
294+
},
295+
grault: {
296+
bar: false,
297+
qux: { foo: true },
298+
},
299+
});
300+
// Subset with circular reference
301+
assertObjectMatch(e, {
302+
foo: true,
303+
bar: {
304+
bar: {
305+
bar: {
306+
foo: true,
307+
},
308+
},
309+
},
310+
});
311+
// Subset with same symbol
312+
assertObjectMatch(f, {
313+
[sym]: true,
314+
});
315+
// Missing key
316+
{
317+
let didThrow;
318+
try {
319+
assertObjectMatch({
320+
foo: true,
321+
}, {
322+
foo: true,
323+
bar: false,
324+
});
325+
didThrow = false;
326+
} catch (e) {
327+
assert(e instanceof AssertionError);
328+
didThrow = true;
329+
}
330+
assertEquals(didThrow, true);
331+
}
332+
// Simple subset
333+
{
334+
let didThrow;
335+
try {
336+
assertObjectMatch(a, {
337+
foo: false,
338+
});
339+
didThrow = false;
340+
} catch (e) {
341+
assert(e instanceof AssertionError);
342+
didThrow = true;
343+
}
344+
assertEquals(didThrow, true);
345+
}
346+
// Subset with another subset
347+
{
348+
let didThrow;
349+
try {
350+
assertObjectMatch(b, {
351+
foo: true,
352+
baz: { bar: true },
353+
});
354+
didThrow = false;
355+
} catch (e) {
356+
assert(e instanceof AssertionError);
357+
didThrow = true;
358+
}
359+
assertEquals(didThrow, true);
360+
}
361+
// Subset with multiple subsets
362+
{
363+
let didThrow;
364+
try {
365+
assertObjectMatch(c, {
366+
foo: true,
367+
baz: { bar: false },
368+
qux: {
369+
baz: { foo: false },
370+
},
371+
});
372+
didThrow = false;
373+
} catch (e) {
374+
assert(e instanceof AssertionError);
375+
didThrow = true;
376+
}
377+
assertEquals(didThrow, true);
378+
}
379+
// Subset with same object reference as subset
380+
{
381+
let didThrow;
382+
try {
383+
assertObjectMatch(d, {
384+
corge: {
385+
foo: true,
386+
qux: { bar: true },
387+
},
388+
grault: {
389+
bar: false,
390+
qux: { foo: false },
391+
},
392+
});
393+
didThrow = false;
394+
} catch (e) {
395+
assert(e instanceof AssertionError);
396+
didThrow = true;
397+
}
398+
assertEquals(didThrow, true);
399+
}
400+
// Subset with circular reference
401+
{
402+
let didThrow;
403+
try {
404+
assertObjectMatch(e, {
405+
foo: true,
406+
bar: {
407+
bar: {
408+
bar: {
409+
foo: false,
410+
},
411+
},
412+
},
413+
});
414+
didThrow = false;
415+
} catch (e) {
416+
assert(e instanceof AssertionError);
417+
didThrow = true;
418+
}
419+
assertEquals(didThrow, true);
420+
}
421+
// Subset with symbol key but with string key subset
422+
{
423+
let didThrow;
424+
try {
425+
assertObjectMatch(f, {
426+
foo: true,
427+
});
428+
didThrow = false;
429+
} catch (e) {
430+
assert(e instanceof AssertionError);
431+
didThrow = true;
432+
}
433+
assertEquals(didThrow, true);
434+
}
435+
});
436+
262437
Deno.test("testingAssertsUnimplemented", function (): void {
263438
let didThrow = false;
264439
try {

0 commit comments

Comments
 (0)