Skip to content
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(std/testing): Add support for object assertion against object subset #8001

Merged
merged 5 commits into from
Oct 21, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions std/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
38 changes: 38 additions & 0 deletions std/testing/asserts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,44 @@ export function assertNotMatch(
}
}

/**
* Make an assertion that `actual` object is a subset of `expected` object, deeply.
* If not, then throw.
*/
export function assertObjectMatch(
actual: { [key: string]: unknown },
expected: { [key: string]: unknown },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should symbols be allowed?

): void {
type loose = { [key: string]: 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);
// Iterate through keys present in both actual and expected
// and filter recursively on object references
const filtered = {} as loose;
for (
const [key, value] of Object.entries(a).filter(([key]) => key in b)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think Object.entries() includes symbols. This would be shown if the tests included an erroneous case for symbols. You can use [...Object.getOwnPropertyNames(a), ...Object.getOwnPropertySymbols(a)] to get a complete list of keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced Object.entries() with your suggestion and it works like a charm, and changed string|symbol by PropertyKey which seems more accurate.

I got some trouble with using symbol as index signature so I casted them into string but not sure it's the cleanest way...

I wanted to add an erroneous case like you suggested, but seems that assertEquals does not handle symbols perfectly currently (unless I missed something) :

import { assertEquals } from "https://deno.land/std/testing/asserts.ts"
const foo = Symbol("foo")
Deno.test("symbol1", () => assertEquals({[foo]:"bar"}, {[foo]:"bar"}))
Deno.test("symbol2", () => assertEquals({[foo]:"bar"}, {[Symbol("foo")]:"bar"}))
running 2 tests
test symbol1 ... ok (2ms)
test symbol2 ... ok (1ms)

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (4ms)

) {
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
155 changes: 155 additions & 0 deletions std/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,160 @@ Deno.test("testingAssertStringNotMatchingThrows", function (): void {
assert(didThrow);
});

Deno.test("testingAssertObjectMatching", function (): void {
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;
// 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,
},
},
},
});
// 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);
}
});

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