Skip to content

Commit

Permalink
feat(mock): add onceImplementation function to mock object
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed Dec 13, 2021
1 parent e93b4f1 commit bada13e
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 15 deletions.
52 changes: 38 additions & 14 deletions mock/fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,36 @@

import { Mock } from "./mock.ts";
import { incrementalNumber } from "./utils.ts";
import { head } from "../matcher/utils.ts";
import type { MockObject } from "./mock.ts";

/** make mock object */
/** store fn internal implementation */
class MockFnStore {
private onceImplementations: ((...args: unknown[]) => unknown)[] = [];
constructor(
private defaultImplementation?: ((...args: unknown[]) => unknown),
) {}

pickImplementation(): ((...args: unknown[]) => unknown) | undefined {
return this.onceImplementations.shift() ?? this.defaultImplementation;
}
}

/** make mock object with implementation function */
function fn<A extends readonly unknown[], R>(
implementation: (...args: A) => R,
): MockObject<A, R>;
/** make mock object */
function fn(): MockObject;
function fn(
implementation?: (...args: unknown[]) => unknown,
): MockObject {
const mock = new Mock();
let implementations:
({ implementation: (...args: unknown[]) => unknown } & IsDefault)[] =
implementation ? [{ isDefault: true, implementation }] : [];
const mockFnStore = new MockFnStore(implementation);

/** Calls a method of an object, substituting another object for the current object. */
const call = (...args: unknown[]): unknown => {
const value = head(implementations)?.implementation?.(...args);
const implementation = mockFnStore.pickImplementation();
const value = implementation?.(...args);
mock.add({
args,
result: {
Expand All @@ -33,10 +44,22 @@ function fn(
return value;
};

/** Sets the default mock function. The set function will be called when the mock object is called. */
const setImplementation = (
implementation: (...args: unknown[]) => unknown,
): MockObject => {
implementations = [{ implementation, isDefault: true }];
mockFnStore["defaultImplementation"] = implementation;
return call as MockObject;
};

/** Sets a mock function to be called only once.
* This takes precedence over the default mock function.
* If there is more than one once implementation, they will be called in the order of registration.
*/
const onceImplementation = (
implementation: (...args: unknown[]) => unknown,
): MockObject => {
mockFnStore["onceImplementations"].unshift(implementation);
return call as MockObject;
};

Expand All @@ -47,16 +70,17 @@ function fn(
},
});

Object.defineProperty(call, "setImplementation", {
value: setImplementation,
Object.defineProperties(call, {
setImplementation: {
value: setImplementation,
},
onceImplementation: {
value: onceImplementation,
},
});

return call as MockObject;
}

export { fn };
export { fn, MockFnStore };
export type { MockObject };

type IsDefault = {
isDefault: boolean;
};
76 changes: 75 additions & 1 deletion mock/fn_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright 2021-Present the Unitest authors. All rights reserved. MIT license.

import { fn } from "./fn.ts";
import { fn, MockFnStore } from "./fn.ts";
import { isFunction } from "../deps.ts";
import { assert, assertEquals, assertExists } from "../dev_deps.ts";

Expand Down Expand Up @@ -77,3 +77,77 @@ Deno.test("setImplementation", () => {
assertEquals(f1.mock.calls.length, 1);
assertEquals(f2.mock.calls.length, 1);
});

Deno.test("onceImplementation should call only one time", () => {
assertExists(fn().onceImplementation);

const mockObject = fn();

const onceImplementation = fn();
mockObject.onceImplementation(onceImplementation);

assertEquals(mockObject(), undefined);
assertEquals(onceImplementation.mock.calls.length, 1);
mockObject();
assertEquals(onceImplementation.mock.calls.length, 1);
});

Deno.test("onceImplementation should be called in preference to default implementation", () => {
const defaultImplementation = fn();
const onceImplementation = fn();
const mockObject = fn(defaultImplementation).onceImplementation(
onceImplementation,
);

assertEquals(defaultImplementation.mock.calls.length, 0);
assertEquals(onceImplementation.mock.calls.length, 0);

mockObject();

assertEquals(defaultImplementation.mock.calls.length, 0);
assertEquals(onceImplementation.mock.calls.length, 1);

mockObject();
assertEquals(defaultImplementation.mock.calls.length, 1);
assertEquals(onceImplementation.mock.calls.length, 1);

mockObject();
assertEquals(defaultImplementation.mock.calls.length, 2);
assertEquals(onceImplementation.mock.calls.length, 1);
});

Deno.test("MockFnStore", () => {
const store = new MockFnStore();
assertExists(store["pickImplementation"]);
assertEquals(store["onceImplementations"], []);
assertEquals(store["defaultImplementation"], undefined);
assertEquals(store.pickImplementation(), undefined);
});

Deno.test("MockFnStore should return picked implementation", () => {
const mockObject = fn();
const store = new MockFnStore(mockObject);

assertEquals(
store["defaultImplementation"],
mockObject,
);
assertEquals(
store.pickImplementation(),
mockObject,
);
assertEquals(
store.pickImplementation(),
mockObject,
);
const mockObject2 = fn();
store["onceImplementations"].unshift(mockObject2);
assertEquals(
store.pickImplementation(),
mockObject2,
);
assertEquals(
store.pickImplementation(),
mockObject,
);
});
3 changes: 3 additions & 0 deletions mock/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ interface MockObject<A extends readonly unknown[] = any[], R = unknown> {
setImplementation(
implementation: (...args: A) => R,
): MockObject<A, R>;
onceImplementation(
implementation: (...args: A) => R,
): MockObject<A, R>;
}

/** mock result store */
Expand Down

0 comments on commit bada13e

Please sign in to comment.