Extract from devtools project

This commit is contained in:
Michaël Lemaire 2021-09-05 19:55:39 +02:00
commit 60e678d0cf
25 changed files with 2805 additions and 0 deletions

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*.{ts,json}]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
deno.d.ts
.vscode
.local

34
README.md Normal file
View file

@ -0,0 +1,34 @@
# typescript/testing
[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/testing?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=testing)
## About
Testing utilities for Deno, trying to stick to jasmine/jest syntax for easy
porting.
## Import
In deno:
```typescript
import { describe, expect, it } from "https://js.thunderk.net/testing/mod.ts";
```
## Use
Syntax is similar to [Jest](https://jestjs.io), and tries to be mostly
compatible.
```typescript
describe("a module", () => {
it("does something", () => {
expect(5).toBeGreatherThan(4);
});
};
```
## Credits
- Embeds a slighly adapted version of [expect](https://github.com/allain/expect)
library from Allain Lalonde

3
TODO.md Normal file
View file

@ -0,0 +1,3 @@
# TODO
- Nested describe blocks

0
deps.ts Normal file
View file

4
doc/about.md Normal file
View file

@ -0,0 +1,4 @@
## About
Testing utilities for Deno, trying to stick to jasmine/jest syntax for easy
porting.

4
doc/credits.md Normal file
View file

@ -0,0 +1,4 @@
## Credits
- Embeds a slighly adapted version of [expect](https://github.com/allain/expect)
library from Allain Lalonde

7
doc/import.md Normal file
View file

@ -0,0 +1,7 @@
## Import
In deno:
```typescript
import { describe, expect, it } from "https://js.thunderk.net/testing/mod.ts";
```

4
doc/index Normal file
View file

@ -0,0 +1,4 @@
about
import
use
credits

12
doc/use.md Normal file
View file

@ -0,0 +1,12 @@
## Use
Syntax is similar to [Jest](https://jestjs.io), and tries to be mostly
compatible.
```typescript
describe("a module", () => {
it("does something", () => {
expect(5).toBeGreatherThan(4);
});
};
```

5
mod.ts Normal file
View file

@ -0,0 +1,5 @@
// Testing tools, trying to stick to jasmine/jest syntax for easy porting.
export { describe, it } from "./src/blocks.ts";
export { expect } from "./src/assertions.ts";
export { mock, mockfn, patch } from "./src/mock.ts";

19
run Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
# Simplified run tool for deno commands
if test $# -eq 0
then
echo "Usage: $0 [file or command]"
exit 1
elif echo $1 | grep -q '.*.ts'
then
denocmd=run
denoargs=$1
shift
else
denocmd=$1
shift
fi
denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@"
exec deno $denocmd $denoargs

20
src/assertions.test.ts Normal file
View file

@ -0,0 +1,20 @@
import { describe, it } from "./blocks.ts";
import { expect } from "./assertions.ts";
describe("expect", () => {
it("checks correctly for set equality", () => {
expect(() => {
expect(new Set([1, 2])).toEqual(new Set([2, 1]));
}).not.toThrow();
expect(() => {
expect(new Set([1])).toEqual([1]);
}).toThrow();
expect(() => {
expect(new Set([1])).toEqual(new Set([2]));
}).toThrow();
expect(() => {
expect(new Set([1])).toEqual({});
}).toThrow();
});
});

8
src/assertions.ts Normal file
View file

@ -0,0 +1,8 @@
import { expect as _expect } from "./expect/expect.ts";
export function expect(value: any): ReturnType<typeof _expect> {
if (value && value.hasOwnProperty("_toExpected")) {
value = value._toExpected();
}
return _expect(value);
}

24
src/blocks.ts Normal file
View file

@ -0,0 +1,24 @@
var _last_title = "";
// Test block, containing one or more it() calls.
export function describe(
title: string | { new (...args: any[]): any } | Function,
body: Function,
) {
switch (typeof title) {
case "string": {
_last_title = title;
break;
}
case "function": {
_last_title = title.name;
break;
}
}
body();
}
// Single test.
export function it(title: string, body: () => void | Promise<void>) {
Deno.test(_last_title ? `${_last_title} ${title}` : title, body);
}

15
src/expect/LICENSE Normal file
View file

@ -0,0 +1,15 @@
ISC License
Copyright (c) 2019, Allain Lalonde
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

120
src/expect/expect.ts Normal file
View file

@ -0,0 +1,120 @@
import * as builtInMatchers from "./matchers.ts";
import type { Matcher, Matchers } from "./matchers.ts";
import { AssertionError } from "https://deno.land/std@0.106.0/testing/asserts.ts";
interface Expected {
toBe(candidate: any): void;
toEqual(candidate: any): void;
toBeTruthy(): void;
toBeFalsy(): void;
toBeDefined(): void;
toBeInstanceOf(clazz: any): void;
toBeUndefined(): void;
toBeNull(): void;
toBeNaN(): void;
toMatch(pattern: RegExp | string): void;
toHaveProperty(propName: string): void;
toHaveLength(length: number): void;
toContain(item: any): void;
toThrow(error?: RegExp | string): void;
toBeGreaterThan(number: number): void;
toBeGreaterThanOrEqual(number: number): void;
toBeLessThan(number: number): void;
toBeLessThanOrEqual(number: number): void;
toHaveBeenCalled(): void;
toHaveBeenCalledTimes(number: number): void;
toHaveBeenCalledWith(...args: any[]): void;
toHaveBeenLastCalledWith(...args: any[]): void;
toHaveBeenNthCalledWith(nthCall: number, ...args: any[]): void;
toHaveReturned(): void;
toHaveReturnedTimes(number: number): void;
toHaveReturnedWith(value: any): void;
toHaveLastReturnedWith(value: any): void;
toHaveNthReturnedWith(nthCall: number, value: any): void;
not: Expected;
resolves: Expected;
rejects: Expected;
}
const matchers: Record<any, Matcher> = {
...builtInMatchers,
};
export function expect(value: any): Expected {
let isNot = false;
let isPromised = false;
const self: any = new Proxy(
{},
{
get(_, name) {
if (name === "not") {
isNot = !isNot;
return self;
}
if (name === "resolves") {
if (!(value instanceof Promise)) {
throw new AssertionError("expected value must be a Promise");
}
isPromised = true;
return self;
}
if (name === "rejects") {
if (!(value instanceof Promise)) {
throw new AssertionError("expected value must be a Promise");
}
value = value.then(
(value) => {
throw new AssertionError(
`Promise did not reject. resolved to ${value}`,
);
},
(err) => err,
);
isPromised = true;
return self;
}
const matcher: Matcher = matchers[name as any];
if (!matcher) {
throw new TypeError(
typeof name === "string"
? `matcher not found: ${name}`
: "matcher not found",
);
}
return (...args: any[]) => {
function applyMatcher(value: any, args: any[]) {
if (isNot) {
let result = matcher(value, ...args);
if (result.pass) {
throw new AssertionError("should not " + result.message);
}
} else {
let result = matcher(value, ...args);
if (!result.pass) {
throw new AssertionError(result.message || "Unknown error");
}
}
}
return isPromised
? value.then((value: any) => applyMatcher(value, args))
: applyMatcher(value, args);
};
},
},
);
return self;
}
export function addMatchers(newMatchers: Matchers): void {
Object.assign(matchers, newMatchers);
}

742
src/expect/expect_test.ts Normal file
View file

@ -0,0 +1,742 @@
import {
assertEquals,
AssertionError,
assertThrows,
} from "https://deno.land/std@0.106.0/testing/asserts.ts";
import { addMatchers, expect } from "./expect.ts";
import * as mock from "./mock.ts";
async function assertPass(fn: Function) {
try {
assertEquals(await fn(), undefined);
} catch (err) {
throw new AssertionError(
`expected ${fn.toString()} to pass but it failed with ${err}`,
);
}
}
async function assertAllPass(...fns: Function[]) {
for (let fn of fns) {
await assertPass(fn);
}
}
async function assertFail(fn: Function) {
let thrown = true;
try {
let resolution = await fn();
thrown = false;
throw new AssertionError(
`expected ${fn.toString()} to throw but it resolved with ${resolution}`,
);
} catch (err) {
// expected
if (!thrown) throw err;
}
}
async function assertAllFail(...fns: Function[]) {
for (let fn of fns) {
await assertFail(fn);
}
}
Deno.test({
name: "exportsFunction",
fn: () => {
assertEquals(typeof expect, "function");
},
});
Deno.test({
name: "throwsWhenNoMatcherFound",
fn: () => {
assertThrows(
//@ts-ignore
() => expect(true).toBeFancy(),
TypeError,
"matcher not found: toBeFancy",
);
},
});
Deno.test({
name: "allowsExtendingMatchers",
fn: () => {
addMatchers({
toBeFancy(value: any) {
if (value === "fancy") {
return { pass: true };
} else {
return { pass: false, message: "was not fancy" };
}
},
});
// @ts-ignore
assertPass(() => expect("fancy").toBeFancy());
},
});
Deno.test({
name: "toBe",
fn: async () => {
const obj = {};
assertEquals(typeof expect(obj).toBe, "function");
await assertAllPass(
() => expect(obj).toBe(obj),
() => expect(obj).not.toBe({}),
() => expect(Promise.resolve(1)).resolves.toBe(1),
() => expect(Promise.reject(1)).rejects.toBe(1),
);
await assertFail(() => expect(obj).toBe({}));
await assertFail(() => expect(obj).not.toBe(obj));
},
});
Deno.test({
name: "toEqual",
fn: async () => {
const obj = {};
await assertAllPass(
() => expect(1).toEqual(1),
() => expect(obj).toEqual({}),
() => expect(obj).toEqual(obj),
() => expect({ a: 1 }).toEqual({ a: 1 }),
() => expect([1]).toEqual([1]),
() => expect(Promise.resolve(1)).resolves.toEqual(1),
() => expect(Promise.reject(1)).rejects.toEqual(1),
);
await assertAllFail(
() => expect(1).toEqual(2),
() => expect(1).toEqual(true),
() => expect({}).toEqual(true),
() => expect(1).not.toEqual(1),
() => expect(true).not.toEqual(true),
);
},
});
Deno.test({
name: "resolves",
fn: async () => {
const resolves = expect(Promise.resolve(true)).resolves;
for (let method of ["toEqual", "toBe", "toBeTruthy", "toBeFalsy"]) {
assertEquals(
typeof (resolves as any)[method],
"function",
`missing ${method}`,
);
}
},
});
Deno.test({
name: "rejects",
fn: async () => {
const rejects = expect(Promise.reject(true)).rejects;
for (
let method of ["toEqual", "toBe", "toBeTruthy", "toBeFalsy"]
) {
assertEquals(typeof (rejects as any)[method], "function");
}
},
});
Deno.test({
name: "toBeDefined",
fn: async () => {
await assertAllPass(
() => expect(true).toBeDefined(),
() => expect({}).toBeDefined(),
() => expect([]).toBeDefined(),
() => expect(undefined).not.toBeDefined(),
() => expect(Promise.resolve({})).resolves.toBeDefined(),
() => expect(Promise.reject({})).rejects.toBeDefined(),
);
await assertAllFail(
() => expect(undefined).toBeDefined(),
() => expect(true).not.toBeDefined(),
);
},
});
Deno.test({
name: "toBeUndefined",
fn: async () => {
await assertAllPass(
() => expect(undefined).toBeUndefined(),
() => expect(null).not.toBeUndefined(),
() => expect(Promise.resolve(undefined)).resolves.toBeUndefined(),
() => expect(Promise.reject(undefined)).rejects.toBeUndefined(),
);
await assertAllFail(
() => expect(null).toBeUndefined(),
() => expect(undefined).not.toBeUndefined(),
() => expect(false).toBeUndefined(),
);
},
});
Deno.test({
name: "toBeTruthy",
fn: async () => {
await assertAllPass(
() => expect(true).toBeTruthy(),
() => expect(false).not.toBeTruthy(),
() => expect(Promise.resolve(true)).resolves.toBeTruthy(),
() => expect(Promise.reject(true)).rejects.toBeTruthy(),
);
await assertAllFail(
() => expect(false).toBeTruthy(),
() => expect(true).not.toBeTruthy(),
);
},
});
Deno.test({
name: "toBeFalsy",
fn: async () => {
await assertAllPass(
() => expect(false).toBeFalsy(),
() => expect(true).not.toBeFalsy(),
() => expect(Promise.resolve(false)).resolves.toBeFalsy(),
() => expect(Promise.reject(false)).rejects.toBeFalsy(),
);
await assertAllFail(
() => expect(true).toBeFalsy(),
() => expect(false).not.toBeFalsy(),
);
},
});
Deno.test({
name: "toBeGreaterThan",
fn: async () => {
await assertAllPass(
() => expect(2).toBeGreaterThan(1),
() => expect(1).not.toBeGreaterThan(2),
() => expect(Promise.resolve(2)).resolves.toBeGreaterThan(1),
() => expect(Promise.reject(2)).rejects.toBeGreaterThan(1),
);
await assertAllFail(
() => expect(1).toBeGreaterThan(1),
() => expect(1).toBeGreaterThan(2),
() => expect(2).not.toBeGreaterThan(1),
);
},
});
Deno.test({
name: "toBeLessThan",
fn: async () => {
await assertAllPass(
() => expect(1).toBeLessThan(2),
() => expect(2).not.toBeLessThan(1),
() => expect(Promise.resolve(1)).resolves.toBeLessThan(2),
() => expect(Promise.reject(1)).rejects.toBeLessThan(2),
);
await assertAllFail(
() => expect(1).toBeLessThan(1),
() => expect(2).toBeLessThan(1),
() => expect(1).not.toBeLessThan(2),
);
},
});
Deno.test({
name: "toBeGreaterThanOrEqual",
fn: async () => {
await assertAllPass(
() => expect(2).toBeGreaterThanOrEqual(1),
() => expect(1).toBeGreaterThanOrEqual(1),
() => expect(1).not.toBeGreaterThanOrEqual(2),
() => expect(Promise.resolve(2)).resolves.toBeGreaterThanOrEqual(2),
() => expect(Promise.reject(2)).rejects.toBeGreaterThanOrEqual(2),
);
await assertAllFail(
() => expect(1).toBeGreaterThanOrEqual(2),
() => expect(2).not.toBeGreaterThanOrEqual(1),
);
},
});
Deno.test({
name: "toBeLessThanOrEqual",
fn: async () => {
await assertAllPass(
() => expect(1).toBeLessThanOrEqual(2),
() => expect(1).toBeLessThanOrEqual(1),
() => expect(2).not.toBeLessThanOrEqual(1),
() => expect(Promise.resolve(1)).resolves.toBeLessThanOrEqual(2),
() => expect(Promise.reject(1)).rejects.toBeLessThanOrEqual(2),
);
await assertAllFail(
() => expect(2).toBeLessThanOrEqual(1),
() => expect(1).not.toBeLessThanOrEqual(1),
() => expect(1).not.toBeLessThanOrEqual(2),
);
},
});
Deno.test({
name: "toBeNull",
fn: async () => {
await assertAllPass(
() => expect(null).toBeNull(),
() => expect(undefined).not.toBeNull(),
() => expect(false).not.toBeNull(),
() => expect(Promise.resolve(null)).resolves.toBeNull(),
() => expect(Promise.reject(null)).rejects.toBeNull(),
);
await assertAllFail(
() => expect({}).toBeNull(),
() => expect(null).not.toBeNull(),
() => expect(undefined).toBeNull(),
);
},
});
Deno.test({
name: "toBeInstanceOf",
fn: async () => {
class A {}
class B {}
await assertAllPass(
() => expect(new A()).toBeInstanceOf(A),
() => expect(new A()).not.toBeInstanceOf(B),
);
await assertAllFail(
() => expect({}).toBeInstanceOf(A),
() => expect(null).toBeInstanceOf(A),
);
},
});
Deno.test({
name: "toBeNaN",
fn: async () => {
await assertAllPass(
() => expect(NaN).toBeNaN(),
() => expect(10).not.toBeNaN(),
() => expect(Promise.resolve(NaN)).resolves.toBeNaN(),
);
await assertAllFail(() => expect(10).toBeNaN(), () => expect(10).toBeNaN());
},
});
Deno.test({
name: "toBeMatch",
fn: async () => {
await assertAllPass(
() => expect("hello").toMatch(/^hell/),
() => expect("hello").toMatch("hello"),
() => expect("hello").toMatch("hell"),
);
await assertAllFail(() => expect("yo").toMatch(/^hell/));
},
});
Deno.test({
name: "toHaveProperty",
fn: async () => {
await assertAllPass(() => expect({ a: "10" }).toHaveProperty("a"));
await assertAllFail(() => expect({ a: 1 }).toHaveProperty("b"));
},
});
Deno.test({
name: "toHaveLength",
fn: async () => {
await assertAllPass(
() => expect([1, 2]).toHaveLength(2),
() => expect({ length: 10 }).toHaveLength(10),
);
await assertAllFail(() => expect([]).toHaveLength(10));
},
});
Deno.test({
name: "toContain",
fn: async () => {
await assertAllPass(
() => expect([1, 2, 3]).toContain(2),
() => expect([]).not.toContain(2),
);
await assertAllFail(
() => expect([1, 2, 3]).toContain(4),
() => expect([]).toContain(4),
);
},
});
Deno.test({
name: "toThrow",
fn: async () => {
await assertAllPass(
() =>
expect(() => {
throw new Error("TEST");
}).toThrow("TEST"),
() => expect(Promise.reject(new Error("TEST"))).rejects.toThrow("TEST"),
);
await assertAllFail(() => expect(() => true).toThrow());
},
});
Deno.test({
name: "toHaveBeenCalled",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m(10);
m(20);
expect(m).toHaveBeenCalled();
},
() => {
const m = mock.fn();
expect(m).not.toHaveBeenCalled();
},
);
await assertAllFail(() => {
const m = mock.fn();
expect(m).toHaveBeenCalled();
});
},
});
Deno.test({
name: "toHaveBeenCalledTimes",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
expect(m).toHaveBeenCalledTimes(0);
},
() => {
const m = mock.fn();
m();
m();
expect(m).toHaveBeenCalledTimes(2);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveBeenCalledTimes(1);
},
() => {
const m = mock.fn();
m();
m();
expect(m).toHaveBeenCalledTimes(3);
},
);
},
});
Deno.test({
name: "toHaveBeenCalledWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m(1, 2, 3);
expect(m).toHaveBeenCalledWith(1, 2, 3);
},
() => {
const m = mock.fn();
m(1, 2, 3);
m(2, 3, 4);
expect(m).toHaveBeenCalledWith(1, 2, 3);
expect(m).toHaveBeenCalledWith(2, 3, 4);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveBeenCalledWith(1);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveBeenCalledWith(1);
},
);
},
});
Deno.test({
name: "toHaveBeenLastCalledWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m(1, 2, 3);
expect(m).toHaveBeenLastCalledWith(1, 2, 3);
},
() => {
const m = mock.fn();
m(1, 2, 3);
m(2, 3, 4);
expect(m).not.toHaveBeenLastCalledWith(1, 2, 3);
expect(m).toHaveBeenLastCalledWith(2, 3, 4);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveBeenLastCalledWith(1);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveBeenLastCalledWith(1);
},
);
},
});
Deno.test({
name: "toHaveBeenNthCalledWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m(1, 2, 3);
expect(m).toHaveBeenNthCalledWith(1, 1, 2, 3);
},
() => {
const m = mock.fn();
m(1, 2, 3);
m(2, 3, 4);
expect(m).not.toHaveBeenNthCalledWith(2, 1, 2, 3);
expect(m).toHaveBeenNthCalledWith(2, 2, 3, 4);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveBeenNthCalledWith(1, 1, 2);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveBeenNthCalledWith(1, 1);
},
);
},
});
Deno.test({
name: "toHaveReturnedWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m();
expect(m).toHaveReturnedWith(undefined);
},
() => {
const m = mock.fn(() => true);
m();
expect(m).not.toHaveReturnedWith(false);
expect(m).toHaveReturnedWith(true);
},
() => {
const m = mock.fn(() => {
throw new Error("TEST");
});
try {
m();
} catch (err) {}
expect(m).not.toHaveReturnedWith(10);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveReturnedWith(1);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveReturnedWith(1);
},
);
},
});
Deno.test({
name: "toHaveReturnedTimes",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m();
expect(m).toHaveReturnedTimes(1);
},
() => {
const m = mock.fn(() => true);
expect(m).toHaveReturnedTimes(0);
},
() => {
const m = mock.fn(() => {
throw new Error("TEST");
});
try {
m();
} catch (err) {}
expect(m).toHaveReturnedTimes(0);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveReturnedTimes(1);
},
() => {
const m = mock.fn();
m(2);
expect(m).not.toHaveReturnedTimes(1);
},
);
},
});
Deno.test({
name: "toHaveReturned",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn();
m();
expect(m).toHaveReturned();
},
() => {
const m = mock.fn();
expect(m).not.toHaveReturned();
},
() => {
const m = mock.fn(() => {
throw new Error("TEST");
});
try {
m();
} catch (err) {}
expect(m).not.toHaveReturned();
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveReturned();
},
() => {
const m = mock.fn();
m();
expect(m).not.toHaveReturned();
},
() => {
const m = mock.fn(() => {
throw new Error("TEST");
});
m();
expect(m).not.toHaveReturned();
},
);
},
});
Deno.test({
name: "toHaveLastReturnedWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn((x: number) => x);
m(1);
m(2);
expect(m).toHaveLastReturnedWith(2);
},
() => {
const m = mock.fn((x: number) => x);
m(1);
m(2);
expect(m).toHaveLastReturnedWith(2);
},
);
await assertAllFail(
() => {
const m = mock.fn((x: number) => x);
expect(m).toHaveLastReturnedWith(1);
},
() => {
const m = mock.fn((x: number) => x);
m(2);
expect(m).toHaveLastReturnedWith(1);
},
);
},
});
Deno.test({
name: "toHaveNthReturnedWith",
fn: async () => {
await assertAllPass(
() => {
const m = mock.fn((x: number) => x);
m(1, 2, 3);
expect(m).toHaveNthReturnedWith(1, 1);
},
() => {
const m = mock.fn((x: number) => x);
m(1, 2, 3);
m(2, 3, 4);
expect(m).not.toHaveNthReturnedWith(2, 1);
expect(m).toHaveNthReturnedWith(2, 2);
},
);
await assertAllFail(
() => {
const m = mock.fn();
expect(m).toHaveNthReturnedWith(1, 1);
},
() => {
const m = mock.fn();
m(2);
expect(m).toHaveNthReturnedWith(1, 1);
},
);
},
});

613
src/expect/matchers.ts Normal file
View file

@ -0,0 +1,613 @@
import {
AssertionError,
equal,
} from "https://deno.land/std@0.106.0/testing/asserts.ts";
import {
diff,
DiffResult,
DiffType,
} from "https://deno.land/std@0.106.0/testing/_diff.ts";
import {
bold,
gray,
green,
red,
white,
} from "https://deno.land/std@0.106.0/fmt/colors.ts";
import * as mock from "./mock.ts";
type MatcherState = {
isNot: boolean;
};
export type Matcher = (value: any, ...args: any[]) => MatchResult;
export type Matchers = {
[key: string]: Matcher;
};
export type MatchResult = {
pass: boolean;
message?: string;
};
const ACTUAL = red(bold("actual"));
const EXPECTED = green(bold("expected"));
const CAN_NOT_DISPLAY = "[Cannot display]";
function createStr(v: unknown): string {
try {
return Deno.inspect(v);
} catch (e) {
return red(CAN_NOT_DISPLAY);
}
}
function createColor(diffType: DiffType): (s: string) => string {
switch (diffType) {
case DiffType.added:
return (s: string) => green(bold(s));
case DiffType.removed:
return (s: string) => red(bold(s));
default:
return white;
}
}
function createSign(diffType: DiffType): string {
switch (diffType) {
case DiffType.added:
return "+ ";
case DiffType.removed:
return "- ";
default:
return " ";
}
}
function buildMessage(diffResult: ReadonlyArray<DiffResult<string>>): string {
return diffResult
.map((result: DiffResult<string>) => {
const c = createColor(result.type);
return c(`${createSign(result.type)}${result.value}`);
})
.join("\n");
}
function buildDiffMessage(actual: unknown, expected: unknown) {
const actualString = createStr(actual);
const expectedString = createStr(expected);
let message;
try {
const diffResult = diff(
actualString.split("\n"),
expectedString.split("\n"),
);
return buildMessage(diffResult);
} catch (e) {
return `\n${red(CAN_NOT_DISPLAY)} + \n\n`;
}
}
function buildFail(message: string) {
return {
pass: false,
message,
};
}
export function toBe(actual: any, expected: any): MatchResult {
if (actual === expected) return { pass: true };
return buildFail(
`expect(${ACTUAL}).toBe(${EXPECTED})\n\n${
buildDiffMessage(
actual,
expected,
)
}`,
);
}
export function toEqual(actual: any, expected: any): MatchResult {
if (equal(actual, expected)) return { pass: true };
return buildFail(
`expect(${ACTUAL}).toEqual(${EXPECTED})\n\n${
buildDiffMessage(
actual,
expected,
)
}`,
);
}
export function toBeGreaterThan(actual: any, comparison: number): MatchResult {
if (actual > comparison) return { pass: true };
const actualString = createStr(actual);
const comparisonString = createStr(comparison);
return buildFail(
`expect(${ACTUAL}).toBeGreaterThan(${EXPECTED})\n\n ${
red(
actualString,
)
} is not greater than ${green(comparisonString)}`,
);
}
export function toBeLessThan(actual: any, comparison: number): MatchResult {
if (actual < comparison) return { pass: true };
const actualString = createStr(actual);
const comparisonString = createStr(comparison);
return buildFail(
`expect(${ACTUAL}).toBeLessThan(${EXPECTED})\n\n ${
red(
actualString,
)
} is not less than ${green(comparisonString)}`,
);
}
export function toBeGreaterThanOrEqual(
actual: any,
comparison: number,
): MatchResult {
if (actual >= comparison) return { pass: true };
const actualString = createStr(actual);
const comparisonString = createStr(comparison);
return buildFail(
`expect(${ACTUAL}).toBeGreaterThanOrEqual(${EXPECTED})\n\n ${
red(
actualString,
)
} is not greater than or equal to ${green(comparisonString)}`,
);
}
export function toBeLessThanOrEqual(
actual: any,
comparison: number,
): MatchResult {
if (actual <= comparison) return { pass: true };
const actualString = createStr(actual);
const comparisonString = createStr(comparison);
return buildFail(
`expect(${ACTUAL}).toBeLessThanOrEqual(${EXPECTED})\n\n ${
red(
actualString,
)
} is not less than or equal to ${green(comparisonString)}`,
);
}
export function toBeTruthy(value: any): MatchResult {
if (value) return { pass: true };
const actualString = createStr(value);
return buildFail(`expect(${ACTUAL}).toBeTruthy()
${red(actualString)} is not truthy`);
}
export function toBeFalsy(value: any): MatchResult {
if (!value) return { pass: true };
const actualString = createStr(value);
return buildFail(
`expect(${ACTUAL}).toBeFalsy()\n\n ${red(actualString)} is not falsy`,
);
}
export function toBeDefined(value: unknown): MatchResult {
if (typeof value !== "undefined") return { pass: true };
const actualString = createStr(value);
return buildFail(
`expect(${ACTUAL}).toBeDefined()\n\n ${
red(actualString)
} is not defined`,
);
}
export function toBeUndefined(value: unknown): MatchResult {
if (typeof value === "undefined") return { pass: true };
const actualString = createStr(value);
return buildFail(
`expect(${ACTUAL}).toBeUndefined()\n\n ${
red(
actualString,
)
} is defined but should be undefined`,
);
}
export function toBeNull(value: unknown): MatchResult {
if (value === null) return { pass: true };
const actualString = createStr(value);
return buildFail(
`expect(${ACTUAL}).toBeNull()\n\n ${red(actualString)} should be null`,
);
}
export function toBeNaN(value: unknown): MatchResult {
if (typeof value === "number" && isNaN(value)) return { pass: true };
const actualString = createStr(value);
return buildFail(
`expect(${ACTUAL}).toBeNaN()\n\n ${red(actualString)} should be NaN`,
);
}
export function toBeInstanceOf(value: any, expected: Function): MatchResult {
if (value instanceof expected) return { pass: true };
const actualString = createStr(value);
const expectedString = createStr(expected);
return buildFail(
`expect(${ACTUAL}).toBeInstanceOf(${EXPECTED})\n\n expected ${
green(
expected.name,
)
} but received ${red(actualString)}`,
);
}
export function toMatch(value: any, pattern: RegExp | string): MatchResult {
const valueStr = value.toString();
if (typeof pattern === "string") {
if (valueStr.indexOf(pattern) !== -1) return { pass: true };
const actualString = createStr(value);
const patternString = createStr(pattern);
return buildFail(
`expect(${ACTUAL}).toMatch(${EXPECTED})\n\n expected ${
red(
actualString,
)
} to contain ${green(patternString)}`,
);
} else if (pattern instanceof RegExp) {
if (pattern.exec(valueStr)) return { pass: true };
const actualString = createStr(value);
const patternString = createStr(pattern);
return buildFail(
`expect(${ACTUAL}).toMatch(${EXPECTED})\n\n ${
red(
actualString,
)
} did not match regex ${green(patternString)}`,
);
} else {
return buildFail("Invalid internal state");
}
}
export function toHaveProperty(value: any, propName: string): MatchResult {
if (typeof value === "object" && typeof value[propName] !== "undefined") {
return { pass: true };
}
const actualString = createStr(value);
const propNameString = createStr(propName);
return buildFail(
`expect(${ACTUAL}).toHaveProperty(${EXPECTED})\n\n ${
red(
actualString,
)
} did not contain property ${green(propNameString)}`,
);
}
export function toHaveLength(value: any, length: number): MatchResult {
if (value?.length === length) return { pass: true };
const actualString = createStr(value.length);
const lengthString = createStr(length);
return buildFail(
`expect(${ACTUAL}).toHaveLength(${EXPECTED})\n\n expected array to have length ${
green(
lengthString,
)
} but was ${red(actualString)}`,
);
}
export function toContain(value: any, item: any): MatchResult {
if (value && typeof value.includes === "function" && value.includes(item)) {
return { pass: true };
}
const actualString = createStr(value);
const itemString = createStr(item);
if (value && typeof value.includes === "function") {
return buildFail(
`expect(${ACTUAL}).toContain(${EXPECTED})\n\n ${
red(
actualString,
)
} did not contain ${green(itemString)}`,
);
} else {
return buildFail(
`expect(${ACTUAL}).toContain(${EXPECTED})\n\n expected ${
red(
actualString,
)
} to have an includes method but it is ${green(itemString)}`,
);
}
}
export function toThrow(value: any, error?: RegExp | string): MatchResult {
let fn;
if (typeof value === "function") {
fn = value;
try {
value = value();
} catch (err) {
value = err;
}
}
const actualString = createStr(fn);
const errorString = createStr(error);
if (value instanceof Error) {
if (typeof error === "string") {
if (!value.message.includes(error)) {
return buildFail(
`expect(${ACTUAL}).toThrow(${EXPECTED})\n\nexpected ${
red(
actualString,
)
} to throw error matching ${green(errorString)} but it threw ${
red(
value.toString(),
)
}`,
);
}
} else if (error instanceof RegExp) {
if (!value.message.match(error)) {
return buildFail(
`expect(${ACTUAL}).toThrow(${EXPECTED})\n\nexpected ${
red(
actualString,
)
} to throw error matching ${green(errorString)} but it threw ${
red(
value.toString(),
)
}`,
);
}
}
return { pass: true };
} else {
return buildFail(
`expect(${ACTUAL}).toThrow(${EXPECTED})\n\nexpected ${
red(
actualString,
)
} to throw but it did not`,
);
}
}
function extractMockCalls(
value: any,
name: string,
): { error?: string; calls: mock.MockCall[] | null } {
if (typeof value !== "function") {
return {
calls: null,
error: `${name} only works on mock functions. received: ${value}`,
};
}
const calls = mock.calls(value);
if (calls === null) {
return { calls: null, error: `${name} only works on mock functions` };
}
return { calls };
}
export function toHaveBeenCalled(value: any): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveBeenCalled");
if (error) return buildFail(error);
const actualString = createStr(value);
if (calls && calls.length !== 0) return { pass: true };
return buildFail(
`expect(${ACTUAL}).toHaveBeenCalled()\n\n ${
red(
actualString,
)
} was not called`,
);
}
export function toHaveBeenCalledTimes(value: any, times: number): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveBeenCalledTimes");
if (error) return buildFail(error);
if (!calls) return buildFail("Invalid internal state");
if (calls && calls.length === times) return { pass: true };
return buildFail(
`expect(${ACTUAL}).toHaveBeenCalledTimes(${EXPECTED})\n\n expected ${times} calls but was called: ${calls.length}`,
);
}
export function toHaveBeenCalledWith(value: any, ...args: any[]): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveBeenCalledWith");
if (error) return buildFail(error);
const wasCalledWith = calls && calls.some((c) => equal(c.args, args));
if (wasCalledWith) return { pass: true };
const argsString = createStr(args);
return buildFail(
`expect(${ACTUAL}).toHaveBeenCalledWith(${EXPECTED})\n\n function was not called with: ${
green(
argsString,
)
}`,
);
}
export function toHaveBeenLastCalledWith(
value: any,
...args: any[]
): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveBeenLastCalledWith");
if (error) return buildFail(error);
if (!calls || !calls.length) {
return buildFail(
`expect(${ACTUAL}).toHaveBeenLastCalledWith(...${EXPECTED})\n\n expect last call args to be ${args} but was not called`,
);
}
const lastCall = calls[calls.length - 1];
if (equal(lastCall.args, args)) return { pass: true };
return buildFail(
`expect(${ACTUAL}).toHaveBeenLastCalledWith(...${EXPECTED})\n\n expect last call args to be ${args} but was: ${lastCall.args}`,
);
}
export function toHaveBeenNthCalledWith(
value: any,
nth: number,
...args: any[]
): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveBeenNthCalledWith");
if (error) return buildFail(error);
const nthCall = calls && calls[nth - 1];
if (nthCall) {
if (equal(nthCall.args, args)) return { pass: true };
return buildFail(
`expect(${ACTUAL}).toHaveBeenNthCalledWith(${EXPECTED})\n\n expect ${nth}th call args to be ${args} but was: ${nthCall.args}`,
);
} else {
return buildFail(
`expect(${ACTUAL}).toHaveBeenNthCalledWith(${EXPECTED})\n\n ${nth}th call was not made.`,
);
}
}
export function toHaveReturnedWith(value: any, result: any): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveReturnedWith");
if (error) return buildFail(error);
const wasReturnedWith = calls &&
calls.some((c) => c.returns && equal(c.returned, result));
if (wasReturnedWith) return { pass: true };
return buildFail(
`expect(${ACTUAL}).toHaveReturnedWith(${EXPECTED})\n\n function did not return: ${result}`,
);
}
export function toHaveReturned(value: any): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveReturned");
if (error) return buildFail(error);
if (calls && calls.some((c) => c.returns)) return { pass: true };
// TODO(allain): better messages
return buildFail(`expected function to return but it never did`);
}
// TODO(allain): better messages
export function toHaveLastReturnedWith(value: any, expected: any): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveLastReturnedWith");
if (error) return buildFail(error);
const lastCall = calls && calls[calls.length - 1];
if (!lastCall) {
return buildFail("no calls made to function");
}
if (lastCall.throws) {
return buildFail(`last call to function threw: ${lastCall.thrown}`);
}
if (equal(lastCall.returned, expected)) return { pass: true };
return buildFail(
`expected last call to return ${expected} but returned: ${lastCall.returned}`,
);
}
export function toHaveReturnedTimes(value: any, times: number): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveReturnedTimes");
if (error) return buildFail(error);
const returnCount = calls && calls.filter((c) => c.returns).length;
if (returnCount !== times) {
return buildFail(
`expected ${times} returned times but returned ${returnCount} times`,
);
}
return { pass: true };
}
export function toHaveNthReturnedWith(
value: any,
nth: number,
expected: any,
): MatchResult {
const { calls, error } = extractMockCalls(value, "toHaveNthReturnedWith");
if (error) return buildFail(error);
const nthCall = calls && calls[nth - 1];
if (!nthCall) {
return buildFail(`${nth} calls were now made`);
}
if (nthCall.throws) {
return buildFail(`${nth}th call to function threw: ${nthCall.thrown}`);
}
if (!equal(nthCall.returned, expected)) {
return buildFail(
`expected ${nth}th call to return ${expected} but returned: ${nthCall.returned}`,
);
}
return { pass: true };
}

728
src/expect/matchers_test.ts Normal file
View file

@ -0,0 +1,728 @@
import {
assert,
assertEquals,
} from "https://deno.land/std@0.106.0/testing/asserts.ts";
import * as mock from "./mock.ts";
import {
MatchResult,
toBe,
toBeDefined,
toBeFalsy,
toBeGreaterThan,
toBeInstanceOf,
toBeLessThan,
toBeLessThanOrEqual,
toBeNaN,
toBeNull,
toBeTruthy,
toBeUndefined,
toContain,
toEqual,
toHaveBeenCalled,
toHaveBeenCalledTimes,
toHaveBeenCalledWith,
toHaveBeenLastCalledWith,
toHaveBeenNthCalledWith,
toHaveLastReturnedWith,
toHaveLength,
toHaveNthReturnedWith,
toHaveProperty,
toHaveReturned,
toHaveReturnedTimes,
toHaveReturnedWith,
toMatch,
toThrow,
} from "./matchers.ts";
function assertResult(actual: MatchResult, expected: MatchResult) {
assertEquals(
actual.pass,
expected.pass,
`expected to be ${
expected.pass ? `pass but received: ${actual.message}` : "fail"
}`,
);
if (typeof expected.message !== "undefined") {
assert(!!actual.message, "no message given");
const colourless = actual.message.replace(
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
"",
);
const trim = (x: string) => x.trim().replace(/\s*\n\s+/g, "\n");
assertEquals(trim(colourless), trim(expected.message));
}
}
function assertResultPass(result: any) {
assertResult(result, { pass: true });
}
Deno.test({
name: "toBePass",
fn: () => {
assertResultPass(toBe(10, 10));
},
});
Deno.test({
name: "toBeFail",
fn: () => {
assertResult(toBe(10, 20), {
pass: false,
message: `expect(actual).toBe(expected)
- 10
+ 20`,
});
assertResult(toBe({}, {}), {
pass: false,
message: `expect(actual).toBe(expected)
{}`,
});
},
});
Deno.test({
name: "toEqualPass",
fn: () => {
assertResultPass(toEqual({ a: 1 }, { a: 1 }));
assertResultPass(toEqual(1, 1));
assertResultPass(toEqual([1], [1]));
},
});
Deno.test({
name: "toEqualFail",
fn: () => {
assertResult(toEqual(10, 20), {
pass: false,
message: `expect(actual).toEqual(expected)\n\n- 10\n+ 20`,
});
assertResult(toEqual({ a: 1 }, { a: 2 }), {
pass: false,
message: `expect(actual).toEqual(expected)
- { a: 1 }
+ { a: 2 }`,
});
},
});
Deno.test({
name: "toBeGreaterThanPass",
fn: () => {
assertResultPass(toBeGreaterThan(2, 1));
},
});
Deno.test({
name: "toBeGreaterThanFail",
fn: () => {
assertResult(toBeGreaterThan(1, 2), {
pass: false,
message: `expect(actual).toBeGreaterThan(expected)
1 is not greater than 2`,
});
},
});
Deno.test({
name: "toBeLessThanPass",
fn: () => {
assertResultPass(toBeLessThan(1, 2));
},
});
Deno.test({
name: "toBeLessThanFail",
fn: () => {
assertResult(toBeLessThan(2, 1), {
pass: false,
message: `expect(actual).toBeLessThan(expected)
2 is not less than 1`,
});
},
});
Deno.test({
name: "toBeLessThanOrEqualPass",
fn: () => {
assertResultPass(toBeLessThanOrEqual(1, 2));
},
});
Deno.test({
name: "toBeLessThanOrEqualFail",
fn: () => {
assertResult(toBeLessThanOrEqual(2, 1), {
pass: false,
message: `expect(actual).toBeLessThanOrEqual(expected)
2 is not less than or equal to 1`,
});
},
});
Deno.test({
name: "toBeTruthyPass",
fn: () => {
assertResultPass(toBeTruthy(1));
assertResultPass(toBeTruthy(true));
assertResultPass(toBeTruthy([]));
},
});
Deno.test({
name: "toBeTruthyFail",
fn: () => {
assertResult(toBeTruthy(false), {
pass: false,
message: `expect(actual).toBeTruthy()
false is not truthy`,
});
},
});
Deno.test({
name: "toBeFalsyPass",
fn: () => {
assertResultPass(toBeFalsy(0));
assertResultPass(toBeFalsy(false));
assertResultPass(toBeFalsy(null));
},
});
Deno.test({
name: "toBeFalsyFail",
fn: () => {
assertResult(toBeFalsy(true), {
pass: false,
message: `expect(actual).toBeFalsy()
true is not falsy`,
});
},
});
Deno.test({
name: "toBeDefinedPass",
fn: () => {
assertResultPass(toBeDefined(1));
assertResultPass(toBeDefined({}));
},
});
Deno.test({
name: "toBeDefinedFail",
fn: () => {
assertResult(toBeDefined(undefined), {
pass: false,
message: `expect(actual).toBeDefined()
undefined is not defined`,
});
},
});
Deno.test({
name: "toBeUndefinedPass",
fn: () => {
assertResultPass(toBeUndefined(undefined));
},
});
Deno.test({
name: "toBeUndefinedFail",
fn: () => {
assertResult(toBeUndefined(null), {
pass: false,
message: `expect(actual).toBeUndefined()
null is defined but should be undefined`,
});
},
});
Deno.test({
name: "toBeNullPass",
fn: () => {
assertResultPass(toBeNull(null));
},
});
Deno.test({
name: "toBeNullFail",
fn: () => {
assertResult(toBeNull(10), {
pass: false,
message: `expect(actual).toBeNull()
10 should be null`,
});
},
});
Deno.test({
name: "toBeNaNPass",
fn: () => {
assertResultPass(toBeNaN(NaN));
},
});
Deno.test({
name: "toBeNaNFail",
fn: () => {
assertResult(toBeNaN(10), {
pass: false,
message: `expect(actual).toBeNaN()
10 should be NaN`,
});
},
});
Deno.test({
name: "toBeInstanceOfPass",
fn: () => {
class A {}
const a = new A();
assertResultPass(toBeInstanceOf(a, A));
},
});
Deno.test({
name: "toBeInstanceOfFail",
fn: () => {
class A {}
class B {}
const a = new A();
assertResult(toBeInstanceOf(a, B), {
pass: false,
message: `expect(actual).toBeInstanceOf(expected)
expected B but received A {}`,
});
},
});
Deno.test({
name: "toBeMatchPass",
fn: () => {
assertResultPass(toMatch("hello", "hell"));
assertResultPass(toMatch("hello", /^hell/));
},
});
Deno.test({
name: "toBeMatchFail",
fn: () => {
assertResult(toMatch("yo", "hell"), {
pass: false,
message: `expect(actual).toMatch(expected)
expected "yo" to contain "hell"`,
});
assertResult(toMatch("yo", /^hell/), {
pass: false,
message: `expect(actual).toMatch(expected)
"yo" did not match regex /^hell/`,
});
},
});
Deno.test({
name: "toBeHavePropertyPass",
fn: () => {
assertResultPass(toHaveProperty({ a: 1 }, "a"));
},
});
Deno.test({
name: "toBeHavePropertyFail",
fn: () => {
assertResult(toHaveProperty({ a: 1 }, "b"), {
pass: false,
message: `expect(actual).toHaveProperty(expected)
{ a: 1 } did not contain property "b"`,
});
},
});
Deno.test({
name: "toHaveLengthPass",
fn: () => {
assertResultPass(toHaveLength([], 0));
assertResultPass(toHaveLength([1, 2], 2));
assertResultPass(toHaveLength({ length: 2 }, 2));
},
});
Deno.test({
name: "toBeHaveLengthFail",
fn: () => {
assertResult(toHaveLength([], 1), {
pass: false,
message: `expect(actual).toHaveLength(expected)
expected array to have length 1 but was 0`,
});
},
});
Deno.test({
name: "toContainPass",
fn: () => {
assertResultPass(toContain([1, 2], 2));
assertResultPass(toContain("Hello", "Hell"));
},
});
Deno.test({
name: "toContainFail",
fn: () => {
assertResult(toContain([2, 3], 1), {
pass: false,
message: `expect(actual).toContain(expected)
[ 2, 3 ] did not contain 1`,
});
assertResult(toContain("Hello", "Good"), {
pass: false,
message: `expect(actual).toContain(expected)
"Hello" did not contain "Good"`,
});
assertResult(toContain(false, 1), {
pass: false,
message: `expect(actual).toContain(expected)
expected false to have an includes method but it is 1`,
});
},
});
Deno.test({
name: "toThrowPass",
fn: () => {
assertResultPass(
toThrow(() => {
throw new Error("TEST");
}, "TEST"),
);
assertResultPass(
toThrow(() => {
throw new Error("TEST");
}, /^TEST/),
);
},
});
Deno.test({
name: "toThrowFail",
fn: () => {
assertResult(
toThrow(() => {}, "TEST"),
{
pass: false,
message: `expect(actual).toThrow(expected)
expected [Function] to throw but it did not`,
},
);
assertResult(
toThrow(() => {
throw new Error("BLAH");
}, "TEST"),
{
pass: false,
message: `expect(actual).toThrow(expected)
expected [Function] to throw error matching "TEST" but it threw Error: BLAH`,
},
);
assertResult(
toThrow(() => {
throw new Error("BLAH");
}, /^TEST/),
{
pass: false,
message: `expect(actual).toThrow(expected)
expected [Function] to throw error matching /^TEST/ but it threw Error: BLAH`,
},
);
},
});
Deno.test({
name: "toHaveBeenCalledPass",
fn: () => {
const m = mock.fn();
m(10);
assertResultPass(toHaveBeenCalled(m));
},
});
Deno.test({
name: "toHaveBeenCalledFail",
fn: () => {
const m = mock.fn();
assertResult(toHaveBeenCalled(m), {
pass: false,
message: `expect(actual).toHaveBeenCalled()
[Function: f] was not called`,
});
},
});
Deno.test({
name: "toHaveBeenCalledTimesPass",
fn: () => {
const m = mock.fn();
m(10);
m(12);
assertResultPass(toHaveBeenCalledTimes(m, 2));
},
});
Deno.test({
name: "toHaveBeenCalledTimesFail",
fn: () => {
const m = mock.fn();
m(10);
assertResult(toHaveBeenCalledTimes(m, 2), {
pass: false,
message: `expect(actual).toHaveBeenCalledTimes(expected)
expected 2 calls but was called: 1`,
});
},
});
Deno.test({
name: "toHaveBeenCalledWithPass",
fn: () => {
const m = mock.fn();
m(1, "a");
assertResultPass(toHaveBeenCalledWith(m, 1, "a"));
},
});
Deno.test({
name: "toHaveBeenCalledWithFail",
fn: () => {
const m = mock.fn();
m(1, "a");
assertResult(toHaveBeenCalledWith(m, 2, "b"), {
pass: false,
message: `expect(actual).toHaveBeenCalledWith(expected)
function was not called with: [ 2, "b" ]`,
});
},
});
Deno.test({
name: "toHaveBeenLastCalledWithPass",
fn: () => {
const m = mock.fn();
m(1, "a");
m(2, "b");
m(3, "c");
assertResultPass(toHaveBeenLastCalledWith(m, 3, "c"));
},
});
Deno.test({
name: "toHaveBeenLastCalledWithPass",
fn: () => {
const m = mock.fn();
assertResult(toHaveBeenLastCalledWith(m, 2, "b"), {
pass: false,
message: `expect(actual).toHaveBeenLastCalledWith(...expected)
expect last call args to be 2,b but was not called`,
});
m(1, "a");
m(2, "b");
m(3, "c");
assertResult(toHaveBeenLastCalledWith(m, 2, "b"), {
pass: false,
message: `expect(actual).toHaveBeenLastCalledWith(...expected)
expect last call args to be 2,b but was: 3,c`,
});
},
});
Deno.test({
name: "toHaveBeenNthCalledWithPass",
fn: () => {
const m = mock.fn();
m(1, "a");
m(2, "b");
m(3, "c");
const nthCall = 2;
assertResultPass(toHaveBeenNthCalledWith(m, nthCall, 2, "b"));
},
});
Deno.test({
name: "toHaveBeenNthCalledWithFail",
fn: () => {
const m = mock.fn();
const nthCall = 3;
assertResult(toHaveBeenNthCalledWith(m, nthCall, 2, "b"), {
pass: false,
message: `expect(actual).toHaveBeenNthCalledWith(expected)
3th call was not made.`,
});
m(1, "a");
m(2, "b");
m(3, "c");
assertResult(toHaveBeenNthCalledWith(m, nthCall, 2, "b"), {
pass: false,
message: `expect(actual).toHaveBeenNthCalledWith(expected)
expect 3th call args to be 2,b but was: 3,c`,
});
},
});
Deno.test({
name: "toHaveReturnedWithPass",
fn: () => {
const m = mock.fn(() => true);
m();
assertResultPass(toHaveReturnedWith(m, true));
},
});
Deno.test({
name: "toHaveReturnedWithFail",
fn: () => {
const m = mock.fn(() => true);
m();
assertResult(toHaveReturnedWith(m, false), {
pass: false,
message: `expect(actual).toHaveReturnedWith(expected)
function did not return: false`,
});
},
});
Deno.test({
name: "toHaveReturnedPass",
fn: () => {
const m = mock.fn(() => true);
m();
assertResultPass(toHaveReturned(m));
},
});
Deno.test({
name: "toHaveReturnedFail",
fn: () => {
const m = mock.fn(() => true);
assertResult(toHaveReturned(m), {
pass: false,
message: `expected function to return but it never did`,
});
},
});
Deno.test({
name: "toHaveLastReturnedWithPass",
fn: () => {
const m = mock.fn((arg: boolean) => arg);
m(false);
m(true);
assertResultPass(toHaveLastReturnedWith(m, true));
},
});
Deno.test({
name: "toHaveLastReturnedWithFail",
fn: () => {
const m = mock.fn((arg: boolean) => arg);
assertResult(toHaveLastReturnedWith(m, true), {
pass: false,
message: `no calls made to function`,
});
m(true);
m(false);
assertResult(toHaveLastReturnedWith(m, true), {
pass: false,
message: `expected last call to return true but returned: false`,
});
},
});
Deno.test({
name: "toHaveReturnedTimesPass",
fn: () => {
const m = mock.fn(() => true);
m();
m();
m();
assertResultPass(toHaveReturnedTimes(m, 3));
},
});
Deno.test({
name: "toHaveReturnedTimesFail",
fn: () => {
const m = mock.fn(() => true);
m();
assertResult(toHaveReturnedTimes(m, 3), {
pass: false,
message: `expected 3 returned times but returned 1 times`,
});
},
});
Deno.test({
name: "toHaveNthReturnedWithPass",
fn: () => {
const m = mock.fn((n: number) => n);
m(1);
m(2);
m(3);
const nthCall = 2;
assertResultPass(toHaveNthReturnedWith(m, nthCall, 2));
},
});
Deno.test({
name: "toHaveNthReturnedWithFail",
fn: () => {
const m = mock.fn((n: number) => n);
m(1);
m(2);
m(3);
assertResult(toHaveNthReturnedWith(m, 2, 1), {
pass: false,
message: `expected 2th call to return 1 but returned: 2`,
});
assertResult(toHaveNthReturnedWith(m, 9, 1), {
pass: false,
message: `9 calls were now made`,
});
},
});

57
src/expect/mock.ts Normal file
View file

@ -0,0 +1,57 @@
const MOCK_SYMBOL = Symbol.for("@MOCK");
export type MockCall = {
args: any[];
returned?: any;
thrown?: any;
timestamp: number;
returns: boolean;
throws: boolean;
};
export function fn(...stubs: Function[]) {
const calls: MockCall[] = [];
const f = (...args: any[]) => {
const stub = stubs.length === 1
? // keep reusing the first
stubs[0]
: // pick the exact mock for the current call
stubs[calls.length];
try {
const returned = stub ? stub(...args) : undefined;
calls.push({
args,
returned,
timestamp: Date.now(),
returns: true,
throws: false,
});
return returned;
} catch (err) {
calls.push({
args,
timestamp: Date.now(),
returns: false,
thrown: err,
throws: true,
});
throw err;
}
};
Object.defineProperty(f, MOCK_SYMBOL, {
value: { calls },
writable: false,
});
return f;
}
export function calls(f: Function): MockCall[] {
const mockInfo = (f as any)[MOCK_SYMBOL];
if (!mockInfo) throw new Error("callCount only available on mock functions");
return [...mockInfo.calls];
}

72
src/expect/mock_test.ts Normal file
View file

@ -0,0 +1,72 @@
import {
assert,
assertEquals,
} from "https://deno.land/std@0.106.0/testing/asserts.ts";
import * as mock from "./mock.ts";
Deno.test({
name: "canMockFunctions",
fn: () => {
assertEquals(typeof mock.fn(), "function");
const f = mock.fn();
f(10);
f(20);
const calls = mock.calls(f);
assert(Array.isArray(calls));
assertEquals(calls.length, 2);
assertEquals(calls.map((c: any) => c.args), [[10], [20]]);
assertEquals(calls.map((c: any) => c.returned), [undefined, undefined]);
assert(
calls.map((c: any) => typeof c.timestamp).every((t: string) =>
t === "number"
),
);
},
});
Deno.test({
name: "mockFunctionTracksReturns",
fn: () => {
const f = mock.fn(
() => 1,
() => {
throw new Error("TEST");
},
);
try {
f();
f();
} catch {}
const calls = mock.calls(f);
assert(calls[0].returns);
assert(!calls[0].throws);
assert(!calls[1].returns);
assert(calls[1].throws);
},
});
Deno.test({
name: "mockFunctionCanHaveImplementations",
fn: () => {
const f = mock.fn(
(n: number) => n,
(n: number) => n * 2,
(n: number) => n * 3,
);
f(1);
f(1);
f(1);
f(1);
f(1);
const calls = mock.calls(f);
assertEquals(calls.length, 5);
assertEquals(
calls.map((c: mock.MockCall) => c.returned),
[1, 2, 3, undefined, undefined],
);
},
});

206
src/mock.test.ts Normal file
View file

@ -0,0 +1,206 @@
import { describe, it } from "./blocks.ts";
import { expect } from "./assertions.ts";
import { mock, mockfn, patch } from "./mock.ts";
class Obj {
called = 0;
func(x: number) {
this.called += 1;
return x * 2;
}
}
function checkRestored(o: Obj) {
const called = o.called;
const result = o.func(4);
expect(o.called).toEqual(called + 1);
expect(result).toEqual(8);
}
describe("patch", () => {
it("calls through by default", () => {
const o = new Obj();
patch(o, "func", (mock) => {
const result = o.func(3);
expect(o.called).toEqual(1);
expect(result).toEqual(6);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(3);
});
patch(o, "func", (mock) => {
const result = o.func(1);
expect(o.called).toEqual(2);
expect(result).toEqual(2);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(1);
});
checkRestored(o);
});
it("calls sequential stubs before passing through", () => {
const o = new Obj();
patch(o, "func", (mock) => {
mock.stub((a) => a * 3);
mock.stub((a) => a * 4);
let result = o.func(2);
expect(result).toEqual(6);
result = o.func(2);
expect(result).toEqual(8);
expect(o.called).toEqual(0);
result = o.func(2);
expect(result).toEqual(4);
expect(o.called).toEqual(1);
expect(mock).toHaveBeenCalledTimes(3);
});
checkRestored(o);
});
it("calls stubs in loop", () => {
const o = new Obj();
patch(o, "func", (mock) => {
mock.stub((a) => a * 3, true);
let result = o.func(2);
expect(result).toEqual(6);
result = o.func(2);
expect(result).toEqual(6);
result = o.func(2);
expect(result).toEqual(6);
expect(o.called).toEqual(0);
expect(mock).toHaveBeenCalledTimes(3);
});
checkRestored(o);
});
it("can reset the mock", () => {
const o = new Obj();
patch(o, "func", (mock) => {
mock.stub((a) => a + 1, true);
let result = o.func(3);
expect(o.called).toEqual(0);
expect(result).toEqual(4);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(3);
result = o.func(2);
expect(o.called).toEqual(0);
expect(result).toEqual(3);
expect(mock).toHaveBeenCalledTimes(2);
expect(mock).toHaveBeenCalledWith(2);
mock.reset();
mock.stub((a) => a + 10, true);
result = o.func(4);
expect(o.called).toEqual(0);
expect(result).toEqual(14);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(4);
mock.reset();
result = o.func(4);
expect(o.called).toEqual(1);
expect(result).toEqual(8);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(4);
});
checkRestored(o);
});
});
describe("mock", () => {
it("puts a no-op stub by default", () => {
const o = new Obj();
mock(o, "func", undefined, (mock) => {
let result = o.func(2);
expect(result).toBeUndefined();
expect(o.called).toEqual(0);
expect(mock).toHaveBeenCalledTimes(1);
});
checkRestored(o);
const f = mock(o, "func", undefined, () => o.func(2));
expect(f).toHaveBeenCalledTimes(1);
checkRestored(o);
});
it("stubs with a fixed value", () => {
const o = new Obj();
mock(o, "func", 17, (mock) => {
let result = o.func(2);
expect(result).toBe(17);
result = o.func(2);
expect(result).toBe(17);
expect(o.called).toEqual(0);
expect(mock).toHaveBeenCalledTimes(2);
});
checkRestored(o);
});
it("stubs with a function result", () => {
const o = new Obj();
let i = 9;
mock(o, "func", () => ++i, (mock) => {
let result = o.func(2);
expect(result).toBe(10);
result = o.func(2);
expect(result).toBe(11);
expect(o.called).toEqual(0);
expect(mock).toHaveBeenCalledTimes(2);
});
checkRestored(o);
});
});
describe("mockfn", () => {
it("constructs a simple mock function", () => {
let mock = mockfn();
expect(mock).toHaveBeenCalledTimes(0);
mock();
expect(mock).toHaveBeenCalledTimes(1);
mock = mockfn(() => 2);
expect(mock).toHaveBeenCalledTimes(0);
let result = mock();
expect(result).toEqual(2);
expect(mock).toHaveBeenCalledTimes(1);
result = mock();
expect(result).toEqual(2);
expect(mock).toHaveBeenCalledTimes(2);
mock = mockfn((x: number) => x + 1);
result = mock(5);
expect(result).toEqual(6);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(5);
});
});

86
src/mock.ts Normal file
View file

@ -0,0 +1,86 @@
import { expect } from "./assertions.ts";
import * as _mock from "./expect/mock.ts";
export const mockfn = _mock.fn;
/**
* Patch an object's method
*/
export function patch<
O,
K extends keyof O,
F extends Function & O[K],
>(
obj: O,
method: K,
lifetime: (mock: MockManipulator<F>) => void,
): F {
const orig = obj[method] as F;
let stubs: { impl: F; loop: boolean }[] = [];
const createMock = () =>
mockfn((...args: any[]) => {
if (stubs.length) {
const result = stubs[0].impl(...args);
if (!stubs[0].loop) {
stubs.shift();
}
return result;
} else {
return orig.call(obj, ...args);
}
});
let mock = createMock();
Object.assign(obj, { [method]: mock });
try {
lifetime({
_toExpected() {
return mock;
},
stub(impl?: F, loop = false) {
if (impl) {
stubs.push({ impl, loop });
} else {
stubs.push({ impl: function () {} as any, loop: true });
}
},
reset() {
mock = createMock();
Object.assign(obj, { [method]: mock });
stubs = [];
},
});
} finally {
Object.assign(obj, { [method]: orig });
}
return mock as unknown as F;
}
/**
* Similar to *patch*, but with stub as enforced default
*/
export function mock<
O,
K extends keyof O,
F extends (...args: any) => any & O[K],
>(
obj: O,
method: K,
stub: F | ReturnType<F> | undefined,
lifetime: (mock: MockManipulator<F>) => void,
): F {
return patch(obj, method, (mock) => {
mock.stub(
typeof stub == "function" ? stub as any : (() => stub) as F,
true,
);
lifetime(mock as any);
}) as unknown as F;
}
type MockManipulator<F extends Function> = {
_toExpected: () => Parameters<typeof expect>[0];
reset: () => void;
stub: (impl?: F, loop?: boolean) => void;
};

10
tsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "esnext",
"target": "ESNext",
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"preserveConstEnums": true
}
}