Skip to content

Commit

Permalink
feat(testing): Add support for object assertion against object subset (
Browse files Browse the repository at this point in the history
…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).
  • Loading branch information
lowlighter authored and caspervonb committed Jan 24, 2021
1 parent 8bc7f59 commit 6f5cbb0
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 0 deletions.
2 changes: 2 additions & 0 deletions testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pretty-printed diff of failing assertion.
`expected`.
- `assertArrayContains()` - Make an assertion that `actual` array contains the
`expected` values.
- `assertObjectMatch()` - Make an assertion that `actual` object match
`expected` subset object
- `assertThrows()` - Expects the passed `fn` to throw. If `fn` does not throw,
this function does. Also compares any errors thrown to an optional expected
`Error` class and checks that the error `.message` includes an optional
Expand Down
42 changes: 42 additions & 0 deletions testing/asserts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,48 @@ export function assertNotMatch(
}
}

/**
* Make an assertion that `actual` object is a subset of `expected` object, deeply.
* If not, then throw.
*/
export function assertObjectMatch(
actual: Record<PropertyKey, unknown>,
expected: Record<PropertyKey, unknown>,
): void {
type loose = Record<PropertyKey, unknown>;
const seen = new WeakMap();
return assertEquals(
(function filter(a: loose, b: loose): loose {
// Prevent infinite loop with circular references with same filter
if ((seen.has(a)) && (seen.get(a) === b)) {
return a;
}
seen.set(a, b);
// Filter keys and symbols which are present in both actual and expected
const filtered = {} as loose;
const entries = [
...Object.getOwnPropertyNames(a),
...Object.getOwnPropertySymbols(a),
]
.filter((key) => key in b)
.map((key) => [key, a[key as string]]) as Array<[string, unknown]>;
// Build filtered object and filter recursively on nested objects references
for (const [key, value] of entries) {
if (typeof value === "object") {
const subset = (b as loose)[key];
if ((typeof subset === "object") && (subset)) {
filtered[key] = filter(value as loose, subset as loose);
continue;
}
}
filtered[key] = value;
}
return filtered;
})(actual, expected),
expected,
);
}

/**
* Forcefully throws a failed assertion
*/
Expand Down
175 changes: 175 additions & 0 deletions testing/asserts_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
assertNotEquals,
assertNotMatch,
assertNotStrictEquals,
assertObjectMatch,
assertStrictEquals,
assertStringContains,
assertThrows,
Expand Down Expand Up @@ -259,6 +260,180 @@ Deno.test("testingAssertStringNotMatchingThrows", function (): void {
assert(didThrow);
});

Deno.test("testingAssertObjectMatching", function (): void {
const sym = Symbol("foo");
const a = { foo: true, bar: false };
const b = { ...a, baz: a };
const c = { ...b, qux: b };
const d = { corge: c, grault: c };
const e = { foo: true } as { [key: string]: unknown };
e.bar = e;
const f = { [sym]: true, bar: false };
// Simple subset
assertObjectMatch(a, {
foo: true,
});
// Subset with another subset
assertObjectMatch(b, {
foo: true,
baz: { bar: false },
});
// Subset with multiple subsets
assertObjectMatch(c, {
foo: true,
baz: { bar: false },
qux: {
baz: { foo: true },
},
});
// Subset with same object reference as subset
assertObjectMatch(d, {
corge: {
foo: true,
qux: { bar: false },
},
grault: {
bar: false,
qux: { foo: true },
},
});
// Subset with circular reference
assertObjectMatch(e, {
foo: true,
bar: {
bar: {
bar: {
foo: true,
},
},
},
});
// Subset with same symbol
assertObjectMatch(f, {
[sym]: true,
});
// Missing key
{
let didThrow;
try {
assertObjectMatch({
foo: true,
}, {
foo: true,
bar: false,
});
didThrow = false;
} catch (e) {
assert(e instanceof AssertionError);
didThrow = true;
}
assertEquals(didThrow, true);
}
// Simple subset
{
let didThrow;
try {
assertObjectMatch(a, {
foo: false,
});
didThrow = false;
} catch (e) {
assert(e instanceof AssertionError);
didThrow = true;
}
assertEquals(didThrow, true);
}
// Subset with another subset
{
let didThrow;
try {
assertObjectMatch(b, {
foo: true,
baz: { bar: true },
});
didThrow = false;
} catch (e) {
assert(e instanceof AssertionError);
didThrow = true;
}
assertEquals(didThrow, true);
}
// Subset with multiple subsets
{
let didThrow;
try {
assertObjectMatch(c, {
foo: true,
baz: { bar: false },
qux: {
baz: { foo: false },
},
});
didThrow = false;
} catch (e) {
assert(e instanceof AssertionError);
didThrow = true;
}
assertEquals(didThrow, true);
}
// Subset with same object reference as subset
{
let didThrow;
try {
assertObjectMatch(d, {
corge: {
foo: true,
qux: { bar: true },
},
grault: {
bar: false,
qux: { foo: false },
},
});
didThrow = false;
} catch (e) {
assert(e instanceof AssertionError);
didThrow = true;
}
assertEquals(didThrow, true);
}
// Subset with circular reference
{
let didThrow;
try {
assertObjectMatch(e, {
foo: true,
bar: {
bar: {
bar: {
foo: false,
},
},
},
});
didThrow = false;
} catch (e) {
assert(e instanceof AssertionError);
didThrow = true;
}
assertEquals(didThrow, true);
}
// Subset with symbol key but with string key subset
{
let didThrow;
try {
assertObjectMatch(f, {
foo: true,
});
didThrow = false;
} catch (e) {
assert(e instanceof AssertionError);
didThrow = true;
}
assertEquals(didThrow, true);
}
});

Deno.test("testingAssertsUnimplemented", function (): void {
let didThrow = false;
try {
Expand Down

0 comments on commit 6f5cbb0

Please sign in to comment.