Skip to content

Commit

Permalink
Merge pull request #13 from marchaos/mock_clear_reset
Browse files Browse the repository at this point in the history
added mockClear and mockReset which also works with deep mocks
  • Loading branch information
marchaos committed Jan 31, 2020
2 parents 4ccd97c + 21444de commit 6179ed2
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 17 deletions.
64 changes: 60 additions & 4 deletions README.md
Expand Up @@ -11,6 +11,7 @@
- Ability to mock any interface or object
- calledWith() extension to provide argument specific expectations, which works for objects and functions.
- Extensive Matcher API compatible with Jasmine matchers
- Supports mocking deep objects / classes.
- Familiar Jest like API

## Installation
Expand Down Expand Up @@ -51,6 +52,29 @@ describe('Party Tests', () => {
});
```

## Assigning Mocks with a Type

If you wish to assign a mock to a variable that requires a type in your test, then you should use the MockProxy<> type
given that this will provide the apis for calledWith() and other built-in jest types for providing test functionality.

```ts
import { MockProxy, mock } from 'jest-mock-extended';

describe('test', () => {
let myMock: MockProxy<MyInterface>;

beforeEach(() => {
myMock = mock<MyInterface>();
})

test(() => {
myMock.calledWith(1).mockReturnValue(2);
...
})
});

```

## calledWith() Extension

```jest-mock-extended``` allows for invocation matching expectations. Types of arguments, even when using matchers are type checked.
Expand All @@ -65,13 +89,44 @@ provider.getSongs.calledWith(any()).mockReturnValue(['Saw her standing there']);
provider.getSongs.calledWith(anyString()).mockReturnValue(['Saw her standing there']);

```
You can also use calledWith() on its own to create a jest.fn() with the calledWith extension:
You can also use ```calledWith()``` on its own to create a ```jest.fn()``` with the calledWith extension:

```ts
const fn = calledWithFn();
fn.calledWith(1, 2).mockReturnValue(3);
```

## Clearing / Resetting Mocks

```jest-mock-extended``` exposes a mockClear and mockReset for resetting or clearing mocks with the same
functionality as ```jest.fn()```.

```ts
import { mock, mockClear, mockReset } from 'jest-mock-extended';

describe('test', () => {
const mock: UserService = mock<UserService>();

beforeEach(() => {
mockReset(mock); // or mockClear(mock)
});
...
})
```

## Deep mocks

If your class has objects returns from methods that you would also like to mock, you can use ```mockDeep``` in
replacement for mock.

```ts
import { mockDeep } from 'jest-mock-extended';

const mockObj = mockDeep<Test1>();
mockObj.deepProp.getNumber.calledWith(1).mockReturnValue(4);
expect(mockObj.deepProp.getNumber(1)).toBe(4);
```

## Available Matchers


Expand All @@ -95,9 +150,10 @@ You can also use calledWith() on its own to create a jest.fn() with the calledWi
|notUndefined() | value !== undefined |
|notEmpty() | value !== undefined && value !== null && value !== '' |

## Writing a custom Matcher
## Writing a Custom Matcher

Custom matchers can be written using a ```MatcherCreator```

Custom matchers can be written using a MatcherCreator
```ts
import { MatcherCreator, Matcher } from 'jest-mock-extended';

Expand All @@ -107,7 +163,7 @@ export const myMatcher: MatcherCreator<MyType> = (expectedValue) => new Matcher(
});
```

By default, the expected value and actual value are the same type. In the case where you need to type the expectedValue
By default, the expected value and actual value are the same type. In the case where you need to type the expected value
differently than the actual value, you can use the optional 2 generic parameter:

```ts
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "jest-mock-extended",
"version": "1.0.7",
"version": "1.0.8",
"homepage": "https://github.com/marchaos/jest-mock-extended",
"description": "Type safe mocking extensions for jest",
"files": [
Expand Down
59 changes: 58 additions & 1 deletion src/Mock.spec.ts
@@ -1,4 +1,4 @@
import mock, { mockDeep } from './Mock';
import mock, { mockClear, mockDeep, mockReset } from './Mock';
import { anyNumber } from './Matchers';
import calledWithFn from './CalledWithFn';

Expand Down Expand Up @@ -264,6 +264,7 @@ describe('jest-mock-extended', () => {
await expect(promiseMockObj).resolves.toBeDefined();
await expect(promiseMockObj).resolves.toMatchObject({ id: 17 });
});

test('Can return as Promise.reject', async () => {
const mockError = mock<Error>();
mockError.message = '17';
Expand All @@ -280,6 +281,7 @@ describe('jest-mock-extended', () => {
await expect(promiseMockObj).rejects.toBe(mockError);
await expect(promiseMockObj).rejects.toHaveProperty('message', '17');
});

test('Can mock a then function', async () => {
const mockPromiseObj = Promise.resolve(42);
const mockObj = mock<MockInt>();
Expand All @@ -292,4 +294,59 @@ describe('jest-mock-extended', () => {
await expect(promiseMockObj).resolves.toEqual(42);
});
});

describe('clearing / resetting', () => {
it('mockReset supports jest.fn()', () => {
const fn = jest.fn().mockImplementation(() => true);
expect(fn()).toBe(true);
mockReset(fn);
expect(fn()).toBe(undefined);
});

it('mockClear supports jest.fn()', () => {
const fn = jest.fn().mockImplementation(() => true);
fn();
expect(fn.mock.calls.length).toBe(1);
mockClear(fn);
expect(fn.mock.calls.length).toBe(0);
});

it('mockReset object', () => {
const mockObj = mock<MockInt>();
mockObj.getSomethingWithArgs.calledWith(1, anyNumber()).mockReturnValue(3);
expect(mockObj.getSomethingWithArgs(1, 2)).toBe(3);
mockReset(mockObj);
expect(mockObj.getSomethingWithArgs(1, 2)).toBe(undefined);
});

it('mockClear object', () => {
const mockObj = mock<MockInt>();
mockObj.getSomethingWithArgs.calledWith(1, anyNumber()).mockReturnValue(3);
expect(mockObj.getSomethingWithArgs(1, 2)).toBe(3);
expect(mockObj.getSomethingWithArgs.mock.calls.length).toBe(1);
mockClear(mockObj);
expect(mockObj.getSomethingWithArgs.mock.calls.length).toBe(0);
// Does not clear mock implementations of calledWith
expect(mockObj.getSomethingWithArgs(1, 2)).toBe(3);
});

it('mockReset deep', () => {
const mockObj = mockDeep<Test1>();
mockObj.deepProp.getNumber.calledWith(1).mockReturnValue(4);
expect(mockObj.deepProp.getNumber(1)).toBe(4);
mockReset(mockObj);
expect(mockObj.deepProp.getNumber(1)).toBe(undefined);
});

it('mockClear deep', () => {
const mockObj = mockDeep<Test1>();
mockObj.deepProp.getNumber.calledWith(1).mockReturnValue(4);
expect(mockObj.deepProp.getNumber(1)).toBe(4);
expect(mockObj.deepProp.getNumber.mock.calls.length).toBe(1);
mockClear(mockObj);
expect(mockObj.deepProp.getNumber.mock.calls.length).toBe(0);
// Does not clear mock implementations of calledWith
expect(mockObj.deepProp.getNumber(1)).toBe(4);
});
});
});
57 changes: 47 additions & 10 deletions src/Mock.ts
Expand Up @@ -17,18 +17,50 @@ export interface MockOpts {
deep?: boolean;
}

export const mockClear = (mock: MockProxy<any>) => {
for (let key of Object.keys(mock)) {
if (mock[key]._isMockObject) {
mockClear(mock[key]);
}

if (mock[key]._isMockFunction) {
mock[key].mockClear();
}
}

// This is a catch for if they pass in a jest.fn()
if (!mock._isMockObject) {
return mock.mockClear();
}
};


export const mockReset = (mock: MockProxy<any>) => {
for (let key of Object.keys(mock)) {
if (mock[key]._isMockObject) {
mockReset(mock[key]);
}
if (mock[key]._isMockFunction) {
mock[key].mockReset();
}
}

// This is a catch for if they pass in a jest.fn()
// Worst case, we will create a jest.fn() (since this is a proxy)
// below in the get and call mockReset on it
if (!mock._isMockObject) {
return mock.mockReset();
}
};

export const mockDeep = <T>(mockImplementation?: DeepPartial<T>): MockProxy<T> & T => mock(mockImplementation, { deep: true });

// @ts-ignore
const overrideMockImp = (obj: object, opts) => {
const overrideMockImp = (obj: DeepPartial<any>, opts?: MockOpts) => {
const proxy = new Proxy<MockProxy<any>>(obj, handler(opts));
for (let name of Object.keys(obj)) {
// @ts-ignore
if (typeof obj[name] === 'object' && obj[name] !== null) {
// @ts-ignore
proxy[name] = overrideMockImp(obj[name]);
proxy[name] = overrideMockImp(obj[name], opts);
} else {
// @ts-ignore
proxy[name] = obj[name];
}
}
Expand All @@ -37,8 +69,12 @@ const overrideMockImp = (obj: object, opts) => {
};

const handler = (opts?: MockOpts) => ({
ownKeys (target: MockProxy<any>) {
return Reflect.ownKeys(target);
},

set: (obj: MockProxy<any>, property: ProxiedProperty, value: any) => {
// @ts-ignore
// @ts-ignore All of these ignores are due to https://github.com/microsoft/TypeScript/issues/1863
obj[property] = value;
return true;
},
Expand All @@ -60,10 +96,10 @@ const handler = (opts?: MockOpts) => ({
// an proxy for calls results in the spy check returning true. This is another reason
// why deep is opt in.
if (opts?.deep && property !== 'calls') {
// @ts-ignore
fn.propName = property;
// @ts-ignore
obj[property] = new Proxy<MockProxy<any>>(fn, handler(opts));
// @ts-ignore
obj[property]._isMockObject = true;
} else {
// @ts-ignore
obj[property] = calledWithFn();
Expand All @@ -75,7 +111,8 @@ const handler = (opts?: MockOpts) => ({
});

const mock = <T>(mockImplementation: DeepPartial<T> = {} as DeepPartial<T>, opts?: MockOpts): MockProxy<T> & T => {
// @ts-ignore
// @ts-ignore private
mockImplementation!._isMockObject = true;
return overrideMockImp(mockImplementation, opts);
};

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
@@ -1,3 +1,3 @@
export { default as mock, mockDeep, MockProxy, CalledWithMock } from './Mock';
export { default as mock, mockDeep, MockProxy, CalledWithMock, mockClear, mockReset } from './Mock';
export { default as calledWithFn } from './CalledWithFn';
export * from './Matchers';

0 comments on commit 6179ed2

Please sign in to comment.