Skip to content

Commit ebd204d

Browse files
authored
fix(testing/mock): determine mock mode once (#649)
Mock had a buggy interaction with `fakeArgs` where the the mock was using the mock mode from the arguments altered after mock creation.
1 parent 143122c commit ebd204d

File tree

3 files changed

+33
-22
lines changed

3 files changed

+33
-22
lines changed

core/testing/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@roka/testing",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"exports": {
55
".": "./testing.ts",
66
"./fake": "./fake.ts",

core/testing/mock.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { tempDirectory } from "@roka/fs/temp";
12
import { maybe } from "@roka/maybe";
23
import {
34
assertEquals,
@@ -8,6 +9,7 @@ import {
89
import { dirname, fromFileUrl, join } from "@std/path";
910
import { MockError } from "@std/testing/mock";
1011
import { assertType, type IsExact } from "@std/testing/types";
12+
import { fakeArgs } from "./fake.ts";
1113
import { type Mock, mock } from "./mock.ts";
1214

1315
assertType<
@@ -70,6 +72,20 @@ Deno.test("mock() implements spy like interface", async (t) => {
7072
await assertRejects(() => mocked(2, 4), MockError);
7173
});
7274

75+
Deno.test("mock() determines mode once", async (t) => {
76+
await using directory = await tempDirectory();
77+
const self = { func: async () => await Promise.resolve(42) };
78+
let mocked: ReturnType<typeof mock<typeof self, "func">>;
79+
{
80+
using _ = fakeArgs(["--update"]);
81+
mocked = mock(t, self, "func", { path: directory.path("args.mock") });
82+
}
83+
{
84+
using _ = fakeArgs([]);
85+
assertEquals(await mocked(), 42);
86+
}
87+
});
88+
7389
Deno.test("mock() matches arguments", async (t) => {
7490
const self = {
7591
func: async (...args: (number | undefined)[]) =>

core/testing/mock.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,10 @@ export function mock<
309309
let state: MockState<Input, Output> | undefined;
310310
let errored = false;
311311
const mockContext = MockContext.get();
312+
const mode = options?.mode ??
313+
(Deno.args.some((arg) => arg === "--update" || arg === "-u")
314+
? "update"
315+
: "replay");
312316
const conversion = {
313317
input: {
314318
convert: options?.conversion?.input?.convert ??
@@ -326,9 +330,9 @@ export function mock<
326330
) {
327331
try {
328332
if (stubbed.restored) throw new MockError("Mock already restored");
329-
state = await mockContext.load(context, self, property, options);
333+
state = await mockContext.load(context, self, property, mode, options);
330334
let output: Output;
331-
if (mode(options) === "replay") {
335+
if (mode === "replay") {
332336
const input = await conversion.input.convert(...args);
333337
output = mockContext.replay(state, property, input);
334338
} else {
@@ -356,14 +360,14 @@ export function mock<
356360
const mock = Object.assign(
357361
fake as Self[Prop],
358362
{
359-
mode: mode(options),
363+
mode,
360364
original: stubbed.original as unknown as Self[Prop],
361365
restored: false,
362366
restore() {
363367
stubbed.restore();
364368
if (errored) return;
365369
if (!state) throw new MockError("No calls made");
366-
if (mode(state?.options) === "update") return;
370+
if (mode === "update") return;
367371
if (state.remaining.length > 0) {
368372
throw new MockError(
369373
`Unmatched calls: ${
@@ -385,13 +389,6 @@ export function mock<
385389
});
386390
}
387391

388-
function mode(options: MockOptions | undefined): MockMode {
389-
return options?.mode ??
390-
(Deno.args.some((arg) => arg === "--update" || arg === "-u")
391-
? "update"
392-
: "replay");
393-
}
394-
395392
function mockPath(
396393
context: Deno.TestContext,
397394
options: MockOptions | undefined,
@@ -407,11 +404,8 @@ function mockPath(
407404
}
408405
}
409406

410-
async function checkPermission(
411-
path: string,
412-
options: MockOptions | undefined,
413-
) {
414-
if (mode(options) !== "update") return;
407+
async function checkPermission(path: string, mode: MockMode) {
408+
if (mode !== "update") return;
415409
const permission = await Deno.permissions.query({ name: "write", path });
416410
if (permission.state !== "granted") {
417411
throw new Deno.errors.PermissionDenied(
@@ -447,6 +441,7 @@ interface MockCall<Input, Output> {
447441

448442
interface MockState<Input, Output> {
449443
name: string;
444+
mode: MockMode;
450445
path: string;
451446
options: MockOptions | undefined;
452447
calls: MockCall<Input, Output>[];
@@ -482,19 +477,18 @@ class MockContext {
482477
context: Deno.TestContext,
483478
self: Self,
484479
property: Prop,
480+
mode: MockMode,
485481
options: MockOptions | undefined,
486482
): Promise<MockState<Input, Output>> {
487-
await checkPermission(mockPath(context, options), options);
483+
await checkPermission(mockPath(context, options), mode);
488484
const path = mockPath(context, options);
489485
if (!this.mocks.has(path)) {
490486
const { value: mock, error } = await maybe<
491487
{ mock: Record<string, MockCall<Input, Output>[]> }
492488
>(() => import(toFileUrl(path).toString()));
493489
if (error) {
494490
if (!(error instanceof TypeError)) throw error;
495-
if (mode(options) === "replay") {
496-
throw new MockError(`No mock found: ${path}`);
497-
}
491+
if (mode === "replay") throw new MockError(`No mock found: ${path}`);
498492
}
499493
if (!this.mocks.has(path)) {
500494
this.mocks.set(path, {
@@ -520,6 +514,7 @@ class MockContext {
520514
];
521515
mock.states.set(name, {
522516
name,
517+
mode,
523518
path,
524519
options,
525520
calls: [],
@@ -587,7 +582,7 @@ class MockContext {
587582
const removedNames: string[] = [];
588583
for (const [path, { records, states }] of this.mocks.entries()) {
589584
const updatedStates = Array.from(
590-
states.values().filter((state) => mode(state.options) === "update"),
585+
states.values().filter((state) => state.mode === "update"),
591586
);
592587
if (!updatedStates.length) continue;
593588
const contents = [`export const mock = {};\n`];

0 commit comments

Comments
 (0)