diff --git a/README.md b/README.md index ffb1b7c..a3d4f26 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ import * as cmd from "jsr:@chiezo/amber/cmd"; #### `mock` -Replace Deno.Command: +Replace Deno.Command as a side effect: ```typescript cmd.mock(); @@ -38,31 +38,17 @@ assert(Symbol.dispose in cmd.mock()); Replace Deno.Command inside the callback: ```typescript -using echo = cmd.spy("echo"); - -cmd.use(() => { - new Deno.Command("echo"); -}); +const echo = cmd.spy("echo"); +cmd.use(() => new Deno.Command("echo")); assertSpyCalls(echo, 1); ``` -#### `restore` - -Restore Deno.Command: - -```typescript -cmd.mock(); -cmd.restore(); -assert(Deno.Command === Original); -``` - #### `spy` Create a spy for a command: ```typescript -using echo = cmd.spy("echo"); - +const echo = cmd.spy("echo"); cmd.use(() => new Deno.Command("echo")); assertSpyCalls(echo, 1); ``` @@ -70,10 +56,8 @@ assertSpyCalls(echo, 1); Create multiple spies for different commands separately: ```typescript -using echo = cmd.spy("echo"); - -using ls = cmd.spy("ls"); - +const echo = cmd.spy("echo"); +const ls = cmd.spy("ls"); cmd.use(() => { new Deno.Command("echo"); assertSpyCalls(echo, 1); @@ -86,13 +70,11 @@ cmd.use(() => { #### `stub` -Create a stub for a command with a dummy by default: +Stub a command with the default dummy: ```typescript -using echo = cmd.stub("echo"); - -cmd.mock(); -await new Deno.Command("echo").output(); +const echo = cmd.stub("echo"); +await cmd.use(() => new Deno.Command("echo").output()); assertEquals( Deno.permissions.querySync({ name: "run", command: "echo" }).state, "prompt", @@ -100,7 +82,7 @@ assertEquals( assertSpyCalls(echo, 1); ``` -Create a stub for a command with a fake: +Stub a command with a given fake: ```typescript cmd.stub( @@ -112,8 +94,45 @@ cmd.stub( } }, ); +cmd.use(() => assertThrows(() => new Deno.Command("echo"))); +``` + +#### `restore` + +Restore Deno.Command: + +```typescript cmd.mock(); -assertThrows(() => new Deno.Command("echo")); +cmd.restore(); +assert(Deno.Command === Original); +``` + +Won't dispose spies created: + +```typescript +const echo = cmd.spy("echo"); +cmd.restore(); +cmd.use(() => new Deno.Command("echo")); +assertSpyCalls(echo, 1); +``` + +#### `dispose` + +Restore Deno.Command: + +```typescript +cmd.mock(); +cmd.dispose(); +assert(Deno.Command === Original); +``` + +Dispose spies created: + +```typescript +const echo = cmd.spy("echo"); +cmd.dispose(); +cmd.use(() => new Deno.Command("echo")); +assertSpyCalls(echo, 0); ``` ### File System @@ -149,38 +168,24 @@ fs.use(() => { }); ``` -#### `restore` - -Restore file system functions: - -```typescript -fs.mock(); -fs.restore(); -assert(Deno.readTextFile === original.readTextFile); -assert(Deno.readTextFileSync === original.readTextFileSync); -``` - #### `spy` Spy file system functions: ```typescript -using spy = fs.spy("../"); - -await fs.use(() => Deno.readTextFile("../README.md")); +const spy = fs.spy("."); +await fs.use(() => Deno.readTextFile("./README.md")); assertSpyCalls(spy.readTextFile, 1); ``` Spy multiple paths separately: ```typescript -using cwd = fs.spy("."); - -using root = fs.spy("../"); - -await fs.use(() => Deno.readTextFile("../README.md")); -assertSpyCalls(cwd.readTextFile, 0); -assertSpyCalls(root.readTextFile, 1); +const cwd = fs.spy("."); +const src = fs.spy("./src"); +await fs.use(() => Deno.readTextFile("./README.md")); +assertSpyCalls(cwd.readTextFile, 1); +assertSpyCalls(src.readTextFile, 0); ``` #### `stub` @@ -188,11 +193,10 @@ assertSpyCalls(root.readTextFile, 1); Won't write to the original path: ```typescript -using stub = fs.stub("../"); - -await fs.use(() => Deno.writeTextFile("../test.txt", "amber")); +const stub = fs.stub("."); +await fs.use(() => Deno.writeTextFile("./test.txt", "amber")); assertEquals( - (await Deno.permissions.query({ name: "write", path: "../test.txt" })) + (await Deno.permissions.query({ name: "write", path: "./test.txt" })) .state, "prompt", ); @@ -202,21 +206,19 @@ assertSpyCalls(stub.writeTextFile, 1); Make the original file readable initially (readThrough): ```typescript -using stub = fs.stub("../"); - -await fs.use(() => Deno.readTextFile("../README.md")); +const stub = fs.stub("."); +await fs.use(() => Deno.readTextFile("./README.md")); assertSpyCalls(stub.readTextFile, 1); ``` Make the updated content readable after being written: ```typescript -using _ = fs.stub("../"); - +fs.stub("."); await fs.use(async () => { - await Deno.writeTextFile("../README.md", "amber"); + await Deno.writeTextFile("./README.md", "amber"); assertEquals( - await Deno.readTextFile("../README.md"), + await Deno.readTextFile("./README.md"), "amber", ); }); @@ -225,21 +227,58 @@ await fs.use(async () => { Throw on a file that has not been written if readThrough is disabled: ```typescript -using _ = fs.stub(new URL("../", import.meta.url), { readThrough: false }); - -fs.use(() => assertThrows(() => Deno.readTextFileSync("../README.md"))); +fs.stub(".", { readThrough: false }); +fs.use(() => assertThrows(() => Deno.readTextFileSync("./README.md"))); ``` Stub multiple paths separately: ```typescript -using cwd = fs.stub("."); +const cwd = fs.stub("."); +const src = fs.stub("./src"); +await fs.use(() => Deno.readTextFile("./README.md")); +assertSpyCalls(cwd.readTextFile, 1); +assertSpyCalls(src.readTextFile, 0); +``` -using root = fs.stub("../"); +#### `restore` -await fs.use(() => Deno.readTextFile("../README.md")); -assertSpyCalls(cwd.readTextFile, 0); -assertSpyCalls(root.readTextFile, 1); +Restore file system functions: + +```typescript +fs.mock(); +fs.restore(); +assert(Deno.readTextFile === original.readTextFile); +assert(Deno.readTextFileSync === original.readTextFileSync); +``` + +Won't dispose spies created: + +```typescript +const spy = fs.spy("."); +fs.restore(); +await fs.use(() => Deno.readTextFile("./README.md")); +assertSpyCalls(spy.readTextFile, 1); +``` + +#### `dispose` + +Restore file system functions: + +```typescript +fs.mock(); +fs.dispose(); +assert(Deno.readTextFile === original.readTextFile); +assert(Deno.readTextFileSync === original.readTextFileSync); +``` + +Dispose spies created: + +```typescript +const spy = fs.spy("."); +fs.dispose(); +await fs.use(() => Deno.readTextFile("./README.md")); +assertSpyCalls(spy.readTextFile, 0); ``` ### Utilities @@ -250,13 +289,11 @@ import { all } from "jsr:@chiezo/amber/util"; #### `all` -Mock multiple modules at the same time: +Mock multiple modules simultaneously: ```typescript -using echo = cmd.stub("echo"); - -using root = fs.stub("../"); - +const echo = cmd.stub("echo"); +const root = fs.stub("../"); all(cmd, fs).mock(); new Deno.Command("echo"); assertSpyCalls(echo, 1); @@ -264,13 +301,11 @@ await Deno.readTextFile("../README.md"); assertSpyCalls(root.readTextFile, 1); ``` -Use multiple modules at the same time: +Use multiple modules simultaneously: ```typescript -using echo = cmd.stub("echo"); - -using root = fs.stub("../"); - +const echo = cmd.stub("echo"); +const root = fs.stub("../"); await all(cmd, fs).use(async () => { new Deno.Command("echo"); assertSpyCalls(echo, 1); @@ -286,7 +321,7 @@ await all(cmd, fs).use(async () => { }); ``` -Restore multiple modules at the same time: +Restore multiple modules simultaneously: ```typescript all(cmd, fs).mock(); @@ -294,3 +329,16 @@ all(cmd, fs).restore(); assert(Deno.Command === original.Command); assert(Deno.readTextFile === original.readTextFile); ``` + +Dispose multiple modules simultaneously: + +```typescript +const echo = cmd.spy("echo"); +const root = fs.spy("../"); +all(cmd, fs).mock(); +all(cmd, fs).dispose(); +new Deno.Command("echo"); +assertSpyCalls(echo, 0); +await Deno.readTextFile("../README.md"); +assertSpyCalls(root.readTextFile, 0); +``` diff --git a/src/cmd.ts b/src/cmd.ts index 54a1da2..5b18840 100644 --- a/src/cmd.ts +++ b/src/cmd.ts @@ -1,17 +1,16 @@ import type { ConstructorSpy } from "@std/testing/mock"; import * as std from "@std/testing/mock"; +import { tryFinally } from "./internal.ts"; -export interface CommandSpy +export interface Spy extends Disposable, ConstructorSpy< Deno.Command, [command: Command, options?: Deno.CommandOptions] - > { -} + > {} -export interface CommandStub - extends CommandSpy { +export interface Stub extends Spy { fake: typeof Deno.Command; } @@ -47,7 +46,7 @@ const spies = new Map< export function stub( command: Command, fake: typeof Deno.Command = CommandDummy, -): CommandStub { +): Stub { const spy = std.spy(fake); spies.set(command.toString(), spy); Object.defineProperties(spy, { @@ -64,12 +63,12 @@ export function stub( }, }, }); - return spy as unknown as CommandStub; + return spy as unknown as Stub; } export function spy( command: Command, -): CommandSpy { +): Spy { return stub(command, CommandOriginal); } @@ -91,20 +90,19 @@ export function restore() { Deno.Command = CommandOriginal; } +export function dispose() { + restore(); + spies.clear(); +} + export function mock(): Disposable { Deno.Command = CommandProxy; return { - [Symbol.dispose]() { - restore(); - }, + [Symbol.dispose]: dispose, }; } export function use(fn: () => T): T { mock(); - try { - return fn(); - } finally { - restore(); - } + return tryFinally(fn, restore); } diff --git a/src/cmd_test.ts b/src/cmd_test.ts index afb2b3d..579f44f 100644 --- a/src/cmd_test.ts +++ b/src/cmd_test.ts @@ -1,5 +1,5 @@ import { assert, assertEquals, assertThrows } from "@std/assert"; -import { afterEach, describe, it } from "@std/testing/bdd"; +import { afterAll, afterEach, describe, it } from "@std/testing/bdd"; import { assertSpyCalls } from "@std/testing/mock"; import * as cmd from "./cmd.ts"; @@ -10,7 +10,7 @@ describe("mock", () => { Deno.Command = Original; }); - it("should replace Deno.Command", () => { + it("should replace Deno.Command as a side effect", () => { cmd.mock(); assert(Deno.Command !== Original); }); @@ -21,35 +21,25 @@ describe("mock", () => { }); describe("use", () => { + afterAll(() => cmd.dispose()); + it("should replace Deno.Command inside the callback", () => { - using echo = cmd.spy("echo"); - cmd.use(() => { - new Deno.Command("echo"); - }); + const echo = cmd.spy("echo"); + cmd.use(() => new Deno.Command("echo")); assertSpyCalls(echo, 1); }); }); -describe("restore", () => { - const Original = Deno.Command; - - it("should restore Deno.Command", () => { - cmd.mock(); - cmd.restore(); - assert(Deno.Command === Original); - }); -}); - describe("spy", () => { it("should create a spy for a command", () => { - using echo = cmd.spy("echo"); + const echo = cmd.spy("echo"); cmd.use(() => new Deno.Command("echo")); assertSpyCalls(echo, 1); }); it("should create multiple spies for different commands separately", () => { - using echo = cmd.spy("echo"); - using ls = cmd.spy("ls"); + const echo = cmd.spy("echo"); + const ls = cmd.spy("ls"); cmd.use(() => { new Deno.Command("echo"); assertSpyCalls(echo, 1); @@ -62,14 +52,11 @@ describe("spy", () => { }); describe("stub", () => { - afterEach(() => { - cmd.restore(); - }); + afterEach(() => cmd.dispose()); - it("should create a stub for a command with a dummy by default", async () => { - using echo = cmd.stub("echo"); - cmd.mock(); - await new Deno.Command("echo").output(); + it("should stub a command with the default dummy", async () => { + const echo = cmd.stub("echo"); + await cmd.use(() => new Deno.Command("echo").output()); assertEquals( Deno.permissions.querySync({ name: "run", command: "echo" }).state, "prompt", @@ -77,7 +64,7 @@ describe("stub", () => { assertSpyCalls(echo, 1); }); - it("should create a stub for a command with a fake", () => { + it("should stub a command with a given fake", () => { cmd.stub( "echo", class extends Deno.Command { @@ -87,7 +74,40 @@ describe("stub", () => { } }, ); + cmd.use(() => assertThrows(() => new Deno.Command("echo"))); + }); +}); + +describe("restore", () => { + const Original = Deno.Command; + + it("should restore Deno.Command", () => { + cmd.mock(); + cmd.restore(); + assert(Deno.Command === Original); + }); + + it("should not dispose spies created", () => { + const echo = cmd.spy("echo"); + cmd.restore(); + cmd.use(() => new Deno.Command("echo")); + assertSpyCalls(echo, 1); + }); +}); + +describe("dispose", () => { + const Original = Deno.Command; + + it("should restore Deno.Command", () => { cmd.mock(); - assertThrows(() => new Deno.Command("echo")); + cmd.dispose(); + assert(Deno.Command === Original); + }); + + it("should dispose spies created", () => { + const echo = cmd.spy("echo"); + cmd.dispose(); + cmd.use(() => new Deno.Command("echo")); + assertSpyCalls(echo, 0); }); }); diff --git a/src/fs.ts b/src/fs.ts index 86773df..831e6b6 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -226,7 +226,7 @@ export function mock(): Disposable { } return { [Symbol.dispose]() { - restore(); + dispose(); }, }; } @@ -254,6 +254,11 @@ export function restore() { } } +export function dispose() { + restore(); + spies.clear(); +} + function restoreFsFn( name: T, fn: typeof Deno[T], diff --git a/src/fs_test.ts b/src/fs_test.ts index e5448bb..d9c45c0 100644 --- a/src/fs_test.ts +++ b/src/fs_test.ts @@ -32,41 +32,28 @@ describe("use", () => { }); }); -describe("restore", () => { - const original = { ...Deno }; - - it("should restore file system functions", () => { - fs.mock(); - fs.restore(); - assert(Deno.readTextFile === original.readTextFile); - assert(Deno.readTextFileSync === original.readTextFileSync); - }); -}); - describe("spy", () => { let cwd: string; beforeAll(() => { cwd = Deno.cwd(); - Deno.chdir(new URL(".", import.meta.url)); - }); - - afterAll(() => { - Deno.chdir(cwd); + Deno.chdir(new URL("../", import.meta.url)); }); + afterEach(() => fs.dispose()); + afterAll(() => Deno.chdir(cwd)); it("should spy file system functions", async () => { - using spy = fs.spy("../"); - await fs.use(() => Deno.readTextFile("../README.md")); + const spy = fs.spy("."); + await fs.use(() => Deno.readTextFile("./README.md")); assertSpyCalls(spy.readTextFile, 1); }); it("should spy multiple paths separately", async () => { - using cwd = fs.spy("."); - using root = fs.spy("../"); - await fs.use(() => Deno.readTextFile("../README.md")); - assertSpyCalls(cwd.readTextFile, 0); - assertSpyCalls(root.readTextFile, 1); + const cwd = fs.spy("."); + const src = fs.spy("./src"); + await fs.use(() => Deno.readTextFile("./README.md")); + assertSpyCalls(cwd.readTextFile, 1); + assertSpyCalls(src.readTextFile, 0); }); }); @@ -75,18 +62,16 @@ describe("stub", () => { beforeAll(() => { cwd = Deno.cwd(); - Deno.chdir(new URL(".", import.meta.url)); - }); - - afterAll(() => { - Deno.chdir(cwd); + Deno.chdir(new URL("../", import.meta.url)); }); + afterEach(() => fs.dispose()); + afterAll(() => Deno.chdir(cwd)); it("should not write to the original path", async () => { - using stub = fs.stub("../"); - await fs.use(() => Deno.writeTextFile("../test.txt", "amber")); + const stub = fs.stub("."); + await fs.use(() => Deno.writeTextFile("./test.txt", "amber")); assertEquals( - (await Deno.permissions.query({ name: "write", path: "../test.txt" })) + (await Deno.permissions.query({ name: "write", path: "./test.txt" })) .state, "prompt", ); @@ -94,32 +79,82 @@ describe("stub", () => { }); it("should make the original file readable initially (readThrough)", async () => { - using stub = fs.stub("../"); - await fs.use(() => Deno.readTextFile("../README.md")); + const stub = fs.stub("."); + await fs.use(() => Deno.readTextFile("./README.md")); assertSpyCalls(stub.readTextFile, 1); }); it("should make the updated content readable after being written", async () => { - using _ = fs.stub("../"); + fs.stub("."); await fs.use(async () => { - await Deno.writeTextFile("../README.md", "amber"); + await Deno.writeTextFile("./README.md", "amber"); assertEquals( - await Deno.readTextFile("../README.md"), + await Deno.readTextFile("./README.md"), "amber", ); }); }); it("should throw on a file that has not been written if readThrough is disabled", () => { - using _ = fs.stub(new URL("../", import.meta.url), { readThrough: false }); - fs.use(() => assertThrows(() => Deno.readTextFileSync("../README.md"))); + fs.stub(".", { readThrough: false }); + fs.use(() => assertThrows(() => Deno.readTextFileSync("./README.md"))); }); it("should stub multiple paths separately", async () => { - using cwd = fs.stub("."); - using root = fs.stub("../"); - await fs.use(() => Deno.readTextFile("../README.md")); - assertSpyCalls(cwd.readTextFile, 0); - assertSpyCalls(root.readTextFile, 1); + const cwd = fs.stub("."); + const src = fs.stub("./src"); + await fs.use(() => Deno.readTextFile("./README.md")); + assertSpyCalls(cwd.readTextFile, 1); + assertSpyCalls(src.readTextFile, 0); + }); +}); + +describe("restore", () => { + const original = { ...Deno }; + let cwd: string; + + beforeAll(() => { + cwd = Deno.cwd(); + Deno.chdir(new URL("../", import.meta.url)); + }); + afterAll(() => Deno.chdir(cwd)); + + it("should restore file system functions", () => { + fs.mock(); + fs.restore(); + assert(Deno.readTextFile === original.readTextFile); + assert(Deno.readTextFileSync === original.readTextFileSync); + }); + + it("should not dispose spies created", async () => { + const spy = fs.spy("."); + fs.restore(); + await fs.use(() => Deno.readTextFile("./README.md")); + assertSpyCalls(spy.readTextFile, 1); + }); +}); + +describe("dispose", () => { + const original = { ...Deno }; + let cwd: string; + + beforeAll(() => { + cwd = Deno.cwd(); + Deno.chdir(new URL("../", import.meta.url)); + }); + afterAll(() => Deno.chdir(cwd)); + + it("should restore file system functions", () => { + fs.mock(); + fs.dispose(); + assert(Deno.readTextFile === original.readTextFile); + assert(Deno.readTextFileSync === original.readTextFileSync); + }); + + it("should dispose spies created", async () => { + const spy = fs.spy("."); + fs.dispose(); + await fs.use(() => Deno.readTextFile("./README.md")); + assertSpyCalls(spy.readTextFile, 0); }); }); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..1871d33 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,7 @@ +export interface AbstractMock { + dispose(): void; + mock(): AbstractMock; + restore(): void; + use(fn: () => T): T; + [Symbol.dispose]: () => void; +} diff --git a/src/util.ts b/src/util.ts index 1718f9f..8738c28 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,7 @@ import { tryFinally } from "./internal.ts"; export interface MockModule { + dispose(): void; mock(): Disposable; restore(): void; use(fn: () => T): T; @@ -8,6 +9,9 @@ export interface MockModule { export function all(...mods: MockModule[]): MockModule { return { + dispose() { + mods.forEach((m) => m.dispose()); + }, mock() { mods.forEach((m) => m.mock()); return { diff --git a/src/util_test.ts b/src/util_test.ts index bb96d82..2260ed1 100644 --- a/src/util_test.ts +++ b/src/util_test.ts @@ -15,16 +15,16 @@ describe("all", () => { }); afterEach(() => { - all(cmd, fs).restore(); + all(cmd, fs).dispose(); }); afterAll(() => { Deno.chdir(cwd); }); - it("should mock multiple modules at the same time", async () => { - using echo = cmd.stub("echo"); - using root = fs.stub("../"); + it("should mock multiple modules simultaneously", async () => { + const echo = cmd.stub("echo"); + const root = fs.stub("../"); all(cmd, fs).mock(); @@ -35,9 +35,9 @@ describe("all", () => { assertSpyCalls(root.readTextFile, 1); }); - it("should use multiple modules at the same time", async () => { - using echo = cmd.stub("echo"); - using root = fs.stub("../"); + it("should use multiple modules simultaneously", async () => { + const echo = cmd.stub("echo"); + const root = fs.stub("../"); await all(cmd, fs).use(async () => { new Deno.Command("echo"); @@ -54,10 +54,24 @@ describe("all", () => { }); }); - it("should restore multiple modules at the same time", () => { + it("should restore multiple modules simultaneously", () => { all(cmd, fs).mock(); all(cmd, fs).restore(); assert(Deno.Command === original.Command); assert(Deno.readTextFile === original.readTextFile); }); + + it("should dispose multiple modules simultaneously", async () => { + const echo = cmd.spy("echo"); + const root = fs.spy("../"); + + all(cmd, fs).mock(); + all(cmd, fs).dispose(); + + new Deno.Command("echo"); + assertSpyCalls(echo, 0); + + await Deno.readTextFile("../README.md"); + assertSpyCalls(root.readTextFile, 0); + }); });