summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--std/testing/README.md2
-rw-r--r--std/testing/asserts.ts42
-rw-r--r--std/testing/asserts_test.ts175
3 files changed, 219 insertions, 0 deletions
diff --git a/std/testing/README.md b/std/testing/README.md
index e442ea014..f970471cb 100644
--- a/std/testing/README.md
+++ b/std/testing/README.md
@@ -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
diff --git a/std/testing/asserts.ts b/std/testing/asserts.ts
index f93e043dc..1d58f0a00 100644
--- a/std/testing/asserts.ts
+++ b/std/testing/asserts.ts
@@ -430,6 +430,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
*/
export function fail(msg?: string): void {
diff --git a/std/testing/asserts_test.ts b/std/testing/asserts_test.ts
index 7b57fbfa0..a0db48e7b 100644
--- a/std/testing/asserts_test.ts
+++ b/std/testing/asserts_test.ts
@@ -9,6 +9,7 @@ import {
assertNotEquals,
assertNotMatch,
assertNotStrictEquals,
+ assertObjectMatch,
assertStrictEquals,
assertStringContains,
assertThrows,
@@ -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 {