Skip to content

Commit

Permalink
feat: overload jest.spyOn to spy on prop (#523)
Browse files Browse the repository at this point in the history
* feat: overload jest.spyOn to use jest.spyOnProp as fallback

* chore: remove unused types
  • Loading branch information
iamogbz authored Dec 19, 2020
1 parent 82cbf70 commit 573417f
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 12 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ Determines if the given object property has been mocked.

#### `jest.spyOnProp(object, propertyName)`

**Note**: This is aliased as `jest.spyOn` as of `v1.9.0`, overriding the existing `jest.spyOn` to use `spyOnProp` when spying on a regular object property.

Creates a mock property attached to `object[propertyName]` and returns a mock property spy object, which controls all access to the object property. Repeating spying on the same object property will return the same mocked property spy.

**Note**: By default, `spyOnProp` preserves the object property value. If you want to overwrite the original value, you can use `jest.spyOnProp(object, propertyName).mockValue(customValue)` or [`jest.spyOn(object, methodName, accessType?)`](https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname-accesstype) to spy on a getter or a setter.
Expand All @@ -89,7 +91,7 @@ mockProps.extend(jest);
const video = require("./video");

it("mocks video length", () => {
const spy = jest.spyOnProp(video, "length");
const spy = jest.spyOn(video, "length");
spy.mockValueOnce(200)
.mockValueOnce(400)
.mockValueOnce(600);
Expand Down
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,25 @@ export const extend: ExtendJest = (jestInstance: typeof jest): void => {
const jestClearAll = jestInstance.clearAllMocks;
const jestResetAll = jestInstance.resetAllMocks;
const jestRestoreAll = jestInstance.restoreAllMocks;
const jestSpyOn = jestInstance.spyOn;
Object.assign(jestInstance, {
isMockProp,
clearAllMocks: () => jestClearAll() && clearAllMocks(),
resetAllMocks: () => jestResetAll() && resetAllMocks(),
restoreAllMocks: () => jestRestoreAll() && restoreAllMocks(),
spyOn: <T>(
object: T,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
propName: any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
accessType: any,
) => {
try {
return jestSpyOn(object, propName, accessType);
} catch (e) {
return spyOnProp(object, propName);
}
},
spyOnProp,
});
};
Expand Down
30 changes: 19 additions & 11 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as mockProps from "src/index";
import { Spyable } from "typings/globals";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockObject: Spyable = {
const mockObject = {
fn1: (): string => "fnReturnValue",
prop1: "1",
prop2: 2,
Expand Down Expand Up @@ -37,7 +37,7 @@ it("mock object undefined property", () => {

it("mocks object property value undefined", () => {
const testObject: Record<string, number> = { propUndefined: undefined };
const spy = jest.spyOnProp(testObject, "propUndefined").mockValue(1);
const spy = jest.spyOn(testObject, "propUndefined").mockValue(1);
expect(testObject.propUndefined).toEqual(1);
testObject.propUndefined = 5;
expect(testObject.propUndefined).toEqual(5);
Expand All @@ -62,8 +62,9 @@ it("mocks object property value null", () => {
it("mocks object property value", () => {
const testObject = { ...mockObject };
const mockValue = 99;
const spy = jest.spyOnProp(testObject, "prop1");
const spy = jest.spyOn(testObject, "prop1");
expect(testObject.prop1).toEqual("1");
// @ts-expect-error allow string assignment
testObject.prop1 = mockValue;
expect(testObject.prop1).toEqual(mockValue);
expect(testObject.prop1).toEqual(mockValue);
Expand All @@ -89,7 +90,7 @@ it("mocks object property replaces once", () => {
const testObject = { ...mockObject };
const mockValue1 = 99;
const mockValue2 = 100;
const spy = jest.spyOnProp(testObject, "prop2").mockValueOnce(mockValue1);
const spy = jest.spyOn(testObject, "prop2").mockValueOnce(mockValue1);
spy.mockValueOnce(mockValue2).mockValueOnce(101);
expect(testObject.prop2).toEqual(mockValue1);
expect(testObject.prop2).toEqual(mockValue2);
Expand All @@ -102,7 +103,7 @@ it("mocks object property replaces once", () => {
it("mocks object multiple properties", () => {
const testObject = { ...mockObject };
const mockValue = 99;
const spy = jest.spyOnProp(testObject, "prop1").mockValue(mockValue);
const spy = jest.spyOn(testObject, "prop1").mockValue(mockValue);
jest.spyOnProp(testObject, "prop2").mockValue(mockValue);
spy.mockRestore();
expect(testObject.prop1).toEqual("1");
Expand All @@ -114,7 +115,7 @@ it("mocks object multiple properties", () => {
it("resets mocked object property", () => {
const testObject = { ...mockObject };
const mockValue = 99;
const spy = jest.spyOnProp(testObject, "prop1").mockValue(mockValue);
const spy = jest.spyOn(testObject, "prop1").mockValue(mockValue);
expect(testObject.prop1).toEqual(mockValue);
expect(jest.isMockProp(testObject, "prop1")).toBe(true);
spy.mockReset();
Expand Down Expand Up @@ -143,7 +144,7 @@ it.each`
const testObject = { ...mockObject };
const mockValue1 = 99;
const mockValue2 = 100;
jest.spyOnProp(testObject, "prop1").mockValue(mockValue1);
jest.spyOn(testObject, "prop1").mockValue(mockValue1);
jest.spyOnProp(testObject, "prop2").mockValue(mockValue2);
expect(testObject.prop1).toEqual(mockValue1);
expect(testObject.prop2).toEqual(mockValue2);
Expand All @@ -162,7 +163,7 @@ it("restores mocked object property in jest.restoreAllMocks", () => {
const testObject = { ...mockObject };
const mockValue1 = 99;
const mockValue2 = 100;
jest.spyOnProp(testObject, "prop1").mockValue(mockValue1);
jest.spyOn(testObject, "prop1").mockValue(mockValue1);
jest.spyOnProp(testObject, "prop2").mockValue(mockValue2);
expect(testObject.prop1).toEqual(mockValue1);
expect(testObject.prop2).toEqual(mockValue2);
Expand All @@ -178,7 +179,7 @@ it("restores mocked object property in jest.restoreAllMocks", () => {
it("does not remock object property", () => {
const testObject1 = { ...mockObject };
const mockValue = 99;
const spy1 = jest.spyOnProp(testObject1, "prop1").mockValue(mockValue);
const spy1 = jest.spyOn(testObject1, "prop1").mockValue(mockValue);
expect(testObject1.prop1).toEqual(mockValue);
const testObject2 = testObject1;
const spy2 = jest.spyOnProp(testObject2, "prop1").mockValue(mockValue);
Expand All @@ -194,7 +195,7 @@ it.each([undefined, null, 99, "value", true].map((v) => [v && typeof v, v]))(
(_, v) => {
expect(() =>
// @ts-expect-error primitives not indexable by string
jest.spyOnProp(v, "propName"),
jest.spyOn(v, "propName"),
).toThrowErrorMatchingSnapshot();
},
);
Expand All @@ -213,6 +214,8 @@ it("does not mock object method property", () => {
).toThrowErrorMatchingSnapshot();
expect(jest.isMockProp(mockObject, "fn1")).toBe(false);
expect(mockObject.fn1()).toEqual("fnReturnValue");
jest.spyOn(mockObject, "fn1").mockReturnValue("fnMockReturnValue");
expect(mockObject.fn1()).toEqual("fnMockReturnValue");
});

it("does not mock object getter property", () => {
Expand All @@ -221,6 +224,8 @@ it("does not mock object getter property", () => {
).toThrowErrorMatchingSnapshot();
expect(jest.isMockProp(mockObject, "propZ")).toBe(false);
expect(mockObject.propZ).toEqual("z");
jest.spyOn(mockObject, "propZ", "get").mockReturnValue("Z");
expect(mockObject.propZ).toEqual("Z");
});

it("does not mock object setter property", () => {
Expand All @@ -231,9 +236,12 @@ it("does not mock object setter property", () => {
},
};
expect(() =>
jest.spyOnProp(testObject, "propY"),
jest.spyOn(testObject, "propY"),
).toThrowErrorMatchingSnapshot();
expect(jest.isMockProp(testObject, "propY")).toBe(false);
const setterSpy = jest.spyOn(testObject, "propY", "set");
testObject.propY = 4;
expect(testObject._value).toEqual(4);
expect(setterSpy).toHaveBeenCalledTimes(1);
expect(setterSpy).toHaveBeenLastCalledWith(4);
});
4 changes: 4 additions & 0 deletions typings/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export type IsMockProp = <T, K extends keyof T>(
declare global {
namespace jest {
const isMockProp: IsMockProp;
function spyOn<T, P extends NonFunctionPropertyNames<Required<T>>>(
object: T,
propName: P,
): MockProp<T>;
const spyOnProp: SpyOnProp;
}
}
Expand Down

0 comments on commit 573417f

Please sign in to comment.