Browse Source

Improve patch/mock and add mockable Deno instance

master 1.2.0
Michaël Lemaire 8 months ago
parent
commit
5b3ea1452f
  1. 3
      cli.ts
  2. 4
      expect/expect.ts
  3. 34
      normalize.ts
  4. 20
      system.test.ts
  5. 17
      system.ts
  6. 195
      testing.test.ts
  7. 132
      testing.ts

3
cli.ts

@ -1,8 +1,9 @@
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write
import { normalize } from "./normalize.ts";
import { Sys } from "./system.ts";
if (import.meta.main) {
if (Deno.args[0] == "normalize") {
if (Sys.args[0] == "normalize") {
await normalize();
} else {
console.error("Indicate the tool you want to run");

4
expect/expect.ts

@ -42,6 +42,10 @@ const matchers: Record<any, Matcher> = {
};
export function expect(value: any): Expected {
if (value && value.hasOwnProperty("_toExpected")) {
value = value._toExpected();
}
let isNot = false;
let isPromised = false;
const self: any = new Proxy(

34
normalize.ts

@ -1,9 +1,13 @@
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write
// Normalize deno projects
import { Sys } from "./system.ts";
/**
* Normalize deno projects
*/
async function isDirectory(path: string): Promise<boolean> {
try {
return (await Deno.stat(path)).isDirectory;
return (await Sys.stat(path)).isDirectory;
} catch {
return false;
}
@ -11,7 +15,7 @@ async function isDirectory(path: string): Promise<boolean> {
async function readContent(path: string): Promise<string> {
try {
return await Deno.readTextFile(path);
return await Sys.readTextFile(path);
} catch {
return "";
}
@ -19,21 +23,21 @@ async function readContent(path: string): Promise<string> {
async function ensureDirectory(path: string) {
if (!await isDirectory(path)) {
await Deno.mkdir(path, { recursive: true });
await Sys.mkdir(path, { recursive: true });
}
}
async function updateDenoDefs() {
const process = await Deno.run({
const process = await Sys.run({
cmd: ["deno", "types"],
stdout: "piped",
});
const defs = new TextDecoder("utf-8").decode(await process.output());
await Deno.writeTextFile("deno.d.ts", defs);
await Sys.writeTextFile("deno.d.ts", defs);
}
async function updateEditorConfig() {
await Deno.writeTextFile(
await Sys.writeTextFile(
".editorconfig",
`root = true
@ -49,7 +53,7 @@ trim_trailing_whitespace = true
}
async function updateTsConfig() {
await Deno.writeTextFile(
await Sys.writeTextFile(
"tsconfig.json",
`{
"compilerOptions": {
@ -72,10 +76,10 @@ async function updateVscodeConf() {
const config = JSON.parse(json_config || "{}");
if (!config["deno.enable"]) {
config["deno.enable"] = true;
await Deno.writeTextFile(path, JSON.stringify(config));
await Sys.writeTextFile(path, JSON.stringify(config));
}
await Deno.writeTextFile(
await Sys.writeTextFile(
".vscode/tasks.json",
`{
"version": "2.0.0",
@ -96,7 +100,7 @@ async function updateVscodeConf() {
}
async function updateGitIgnore() {
await Deno.writeTextFile(
await Sys.writeTextFile(
".gitignore",
`deno.d.ts
.vscode
@ -106,7 +110,7 @@ async function updateGitIgnore() {
}
async function updateGitHooks() {
await Deno.writeTextFile(
await Sys.writeTextFile(
".git/hooks/pre-commit",
`#!/bin/sh
set -e
@ -114,12 +118,12 @@ deno fmt --check
deno test
`,
);
await Deno.chmod(".git/hooks/pre-commit", 0o755);
await Sys.chmod(".git/hooks/pre-commit", 0o755);
}
async function updateReadme() {
const project = Deno.cwd().split("/").pop();
await Deno.writeTextFile(
const project = Sys.cwd().split("/").pop();
await Sys.writeTextFile(
"README.md",
`# typescript/${project}

20
system.test.ts

@ -0,0 +1,20 @@
import { Sys } from "./system.ts";
import { describe, expect, it, mock } from "./testing.ts";
describe("Sys", () => {
it("is mockable", () => {
function runSomething() {
Sys.run({ cmd: ["echo", "test"] });
}
expect(runSomething).toThrow(
"access to run a subprocess, run again with the --allow-run flag",
);
mock(Sys, "run", undefined, (run) => {
runSomething();
expect(run).toHaveBeenCalledTimes(1);
expect(run).toHaveBeenCalledWith({ cmd: ["echo", "test"] });
});
});
});

17
system.ts

@ -0,0 +1,17 @@
/**
* System tools (only available when targetting Deno, not on browsers)
*/
/**
* Mockable Deno equivalent
*/
export const Sys: typeof Deno = new Proxy({} as typeof Deno, {
get(obj: any, prop: keyof typeof Deno) {
return obj.hasOwnProperty(prop) ? obj[prop] : Deno[prop];
},
set: function (obj: any, prop, value) {
obj[prop] = value;
console.log(obj, prop);
return true;
},
});

195
testing.test.ts

@ -1,4 +1,4 @@
import { expect, it, mockfn, patch } from "./testing.ts";
import { describe, expect, it, mock, mockfn, patch } from "./testing.ts";
class Obj {
called = 0;
@ -15,100 +15,147 @@ function checkRestored(o: Obj) {
expect(result).toEqual(8);
}
it("patch with call-through", () => {
const o = new Obj();
const mock = patch(o, "func");
mock.exec(() => {
const result = o.func(3);
expect(o.called).toEqual(1);
expect(result).toEqual(6);
expect(mock.fn).toHaveBeenCalledTimes(1);
expect(mock.fn).toHaveBeenCalledWith(3);
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);
});
mock.exec(() => {
const result = o.func(1);
expect(o.called).toEqual(2);
expect(result).toEqual(2);
expect(mock.fn).toHaveBeenCalledTimes(1);
expect(mock.fn).toHaveBeenCalledWith(1);
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);
});
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("patch with single looping stub", () => {
const o = new Obj();
const mock = patch(o, "func", (a) => a * 3);
mock.exec(() => {
let result = o.func(2);
expect(result).toEqual(6);
describe(mock, () => {
it("puts a no-op stub by default", () => {
const o = new Obj();
result = o.func(2);
expect(result).toEqual(6);
mock(o, "func", undefined, (mock) => {
let result = o.func(2);
expect(result).toBeUndefined();
result = o.func(2);
expect(result).toEqual(6);
expect(o.called).toEqual(0);
expect(mock).toHaveBeenCalledTimes(1);
});
expect(o.called).toEqual(0);
expect(mock.fn).toHaveBeenCalledTimes(3);
checkRestored(o);
});
checkRestored(o);
});
it("stubs with a fixed value", () => {
const o = new Obj();
it("patch with stub sequence", () => {
const o = new Obj();
const mock = patch(o, "func", (a) => a * 3, (a) => a * 4);
mock.exec(() => {
let result = o.func(2);
expect(result).toEqual(6);
mock(o, "func", 17, (mock) => {
let result = o.func(2);
expect(result).toBe(17);
result = o.func(2);
expect(result).toEqual(8);
result = o.func(2);
expect(result).toBe(17);
result = o.func(2);
expect(result).toBeUndefined();
expect(o.called).toEqual(0);
expect(mock).toHaveBeenCalledTimes(2);
});
expect(o.called).toEqual(0);
expect(mock.fn).toHaveBeenCalledTimes(3);
checkRestored(o);
});
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);
});
});
it("allows short mock syntax", () => {
const o = new Obj();
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);
patch(o, "func").exec((mock) => {
const result = o.func(3);
expect(o.called).toEqual(1);
mock = mockfn((x: number) => x + 1);
result = mock(5);
expect(result).toEqual(6);
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(3);
expect(mock).toHaveBeenCalledWith(5);
});
checkRestored(o);
});
it("mockfn to construct a mock without patch", () => {
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);
});

132
testing.ts

@ -1,51 +1,117 @@
// Testing tools
export { expect } from "./expect/expect.ts";
import * as mock from "./expect/mock.ts";
/**
* Testing tools, trying to stick to jasmine/jest syntax for easy porting
*/
export type PatchResult = {
exec(body: (fmock: Function) => void): void;
fn: Function;
};
import { expect as _expect } from "./expect/expect.ts";
import * as _mock from "./expect/mock.ts";
import { Sys } from "./system.ts";
var _last_title = "";
export const expect = _expect;
export const mockfn = _mock.fn;
export const mockfn = mock.fn;
var _last_title = "";
/**
* Test block, containing one or more it() calls.
*/
export function describe(
title: string | { new (...args: any[]): any },
title: string | { new (...args: any[]): any } | Function,
body: Function,
) {
_last_title = (typeof title == "string")
? title
: title.prototype.constructor.name;
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);
Sys.test(_last_title ? `${_last_title} ${title}` : title, body);
}
type MockManipulator<F extends Function> = {
_toExpected: () => Parameters<typeof expect>[0];
reset: () => void;
stub: (impl?: F, loop?: boolean) => void;
};
/**
* Patch an object's method
*/
export function patch<
T extends Object,
K extends keyof T,
F extends T[K] & Function,
O,
K extends keyof O,
F extends Function & O[K],
>(
obj: T,
obj: O,
method: K,
...stubs: F[]
): PatchResult {
const result = {
fn: () => {},
exec(body: (fmock: Function) => void) {
const orig = obj[method] as unknown as Function;
result.fn = mockfn(...(stubs.length ? stubs : [orig.bind(obj)]));
Object.assign(obj, { [method]: result.fn });
try {
body(result.fn);
} finally {
Object.assign(obj, { [method]: orig });
lifetime: (mock: MockManipulator<F>) => void,
): F {
const orig = obj[method] as F;
const stubs: { impl: F; loop: boolean }[] = [];
const mock = mockfn((...args: any[]) => {
if (stubs.length) {
const result = stubs[0].impl(...args);
if (!stubs[0].loop) {
stubs.shift();
}
},
};
return result;
return result;
} else {
return orig.call(obj, ...args);
}
});
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() {
// TODO
},
});
} 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;
}) as unknown as F;
}
Loading…
Cancel
Save