Skip to content

Commit

Permalink
feat(MockInstance): console.warn on forgotten resets #857
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Jul 25, 2021
1 parent f9e1117 commit 3e35252
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 18 deletions.
4 changes: 2 additions & 2 deletions docs/articles/api/MockInstance.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,11 @@ There is `MockInstance.scope()` to reduce code to one line:
```ts
describe('suite', () => {
// Uses beforeAll and afterAll.
MockInstance.scope();
MockInstance.scope('all');

describe('sub suite', () => {
// Uses beforeEach and afterEach.
MockInstance.scope('each');
MockInstance.scope();
});
});
```
Expand Down
77 changes: 77 additions & 0 deletions e2e/a-min/src/tests/issue-857/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// tslint:disable no-console

import { Component } from '@angular/core';
import {
MockBuilder,
MockInstance,
MockRender,
ngMocks,
} from 'ng-mocks';

@Component({
selector: 'target',
template: '{{ name }}',
})
class TargetComponent {
public readonly name?: string = 'target';
}

describe('issue-857', () => {
beforeEach(() => MockBuilder(null, TargetComponent));

describe('throws on forgotten resets', () => {
let consoleWarn;
beforeAll(() => {
consoleWarn = console.warn;
console.warn =
typeof jest === 'undefined'
? jasmine.createSpy('console.warn')
: jest.fn().mockName('console.warn');

ngMocks.config({
onMockInstanceRestoreNeed: 'warn',
});
});
afterAll(() => {
MockInstance(TargetComponent);
console.warn = consoleWarn;

ngMocks.config({
onMockInstanceRestoreNeed: null,
});
});

it('internal override', () => {
MockInstance(TargetComponent, 'name', 'mock');
const instance =
MockRender(TargetComponent).point.componentInstance;
expect(instance.name).toEqual('mock');
});

it('default override', () => {
expect(console.warn).toHaveBeenCalledWith(
[
'MockInstance: side effects have been detected (TargetComponent).',
'Forgot to add MockInstance.scope() or to call MockInstance.restore()?',
].join(' '),
);
});
});

describe('respects valid resets', () => {
MockInstance.scope();

it('internal override', () => {
MockInstance(TargetComponent, 'name', 'mock');
const instance =
MockRender(TargetComponent).point.componentInstance;
expect(instance.name).toEqual('mock');
});

it('default override', () => {
const instance =
MockRender(TargetComponent).point.componentInstance;
expect(instance.name).toEqual(undefined);
});
});
});
1 change: 1 addition & 0 deletions libs/ng-mocks/src/lib/common/core.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export default {
'InjectionToken EventManagerPlugins', // EVENT_MANAGER_PLUGINS
'InjectionToken HammerGestureConfig', // HAMMER_GESTURE_CONFIG
],
onMockInstanceRestoreNeed: 'warn',
onTestBedFlushNeed: 'warn',
};
2 changes: 2 additions & 0 deletions libs/ng-mocks/src/lib/common/ng-mocks-universe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ ngMocksUniverse.global = new Map();
ngMocksUniverse.touches = new Set();

ngMocksUniverse.global.set('flags', {
// @deprecated and will be changed in A13 to 'throw'
onMockInstanceRestoreNeed: coreConfig.onMockInstanceRestoreNeed,
// @deprecated and will be changed in A13 to 'throw'
onTestBedFlushNeed: coreConfig.onTestBedFlushNeed,
});
Expand Down
11 changes: 7 additions & 4 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,16 @@ export default {
click: mockHelperClick,
config: (config: {
mockRenderCacheSize?: number | null;
onMockInstanceRestoreNeed?: 'throw' | 'warn' | 'i-know-but-disable' | null;
onTestBedFlushNeed?: 'throw' | 'warn' | 'i-know-but-disable' | null;
}) => {
const flags = ngMocksUniverse.global.get('flags');
if (config.onTestBedFlushNeed === null) {
flags.onTestBedFlushNeed = coreConfig.onTestBedFlushNeed;
} else if (config.onTestBedFlushNeed !== undefined) {
flags.onTestBedFlushNeed = config.onTestBedFlushNeed;
for (const flag of ['onTestBedFlushNeed', 'onMockInstanceRestoreNeed'] as const) {
if (config[flag] === null) {
flags[flag] = coreConfig[flag];
} else if (config[flag] !== undefined) {
flags[flag] = config[flag];
}
}
if (config.mockRenderCacheSize === null) {
ngMocksUniverse.global.delete('mockRenderCacheSize');
Expand Down
1 change: 1 addition & 0 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const ngMocks: {

config(config: {
mockRenderCacheSize?: number | null;
onMockInstanceRestoreNeed?: 'throw' | 'warn' | 'i-know-but-disable' | null;
onTestBedFlushNeed?: 'throw' | 'warn' | 'i-know-but-disable' | null;
}): void;

Expand Down
28 changes: 28 additions & 0 deletions libs/ng-mocks/src/lib/mock-instance/mock-instance-forgot-reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ngMocksUniverse from '../common/ng-mocks-universe';

export default (checkReset: Array<[any, any]>) => {
const showError: string[] = [];

// istanbul ignore next: because of the installed global scope switcher we cannot test this part
while (checkReset.length) {
const [declaration, config] = checkReset.pop() || /* istanbul ignore next */ [];
if (config === ngMocksUniverse.configInstance.get(declaration)) {
showError.push(typeof declaration === 'function' ? declaration.name : declaration);
}
}

// istanbul ignore if: because of the installed global scope switcher we cannot test this part
if (showError.length) {
const globalFlags = ngMocksUniverse.global.get('flags');
const errorMessage = [
`MockInstance: side effects have been detected (${showError.join(', ')}).`,
`Forgot to add MockInstance.scope() or to call MockInstance.restore()?`,
].join(' ');
if (globalFlags.onMockInstanceRestoreNeed === 'warn') {
// tslint:disable-next-line no-console
console.warn(errorMessage);
} else if (globalFlags.onMockInstanceRestoreNeed === 'throw') {
throw new Error(errorMessage);
}
}
};
47 changes: 38 additions & 9 deletions libs/ng-mocks/src/lib/mock-instance/mock-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import funcImportExists from '../common/func.import-exists';
import ngMocksStack, { NgMocksStack } from '../common/ng-mocks-stack';
import ngMocksUniverse from '../common/ng-mocks-universe';

import mockInstanceForgotReset from './mock-instance-forgot-reset';

let currentStack: NgMocksStack;
ngMocksStack.subscribePush(state => {
currentStack = state;
});
ngMocksStack.subscribePop((state, stack) => {
for (const declaration of state.mockInstance || /* istanbul ignore next */ []) {
ngMocksUniverse.configInstance.get(declaration)?.overloads?.pop();
if (ngMocksUniverse.configInstance.has(declaration)) {
const universeConfig = ngMocksUniverse.configInstance.get(declaration);
universeConfig.overloads.pop();
ngMocksUniverse.configInstance.set(declaration, {
...universeConfig,
});
}
}
currentStack = stack[stack.length - 1];
});
Expand Down Expand Up @@ -61,6 +69,17 @@ const parseMockInstanceArgs = (args: any[]): MockInstanceArgs => {
return set;
};

const checkReset: Array<[any, any]> = [];
let checkCollect = false;

// istanbul ignore else: maybe a different runner is used
// tslint:disable-next-line strict-type-predicates
if (typeof beforeEach !== 'undefined') {
beforeEach(() => (checkCollect = true));
beforeEach(() => mockInstanceForgotReset(checkReset));
afterEach(() => (checkCollect = false));
}

const mockInstanceConfig = <T>(declaration: Type<T> | AbstractType<T> | InjectionToken<T>, data?: any): void => {
const config = typeof data === 'function' ? { init: data } : data;
const universeConfig = ngMocksUniverse.configInstance.has(declaration)
Expand All @@ -77,9 +96,13 @@ const mockInstanceConfig = <T>(declaration: Type<T> | AbstractType<T> | Injectio
ngMocksUniverse.configInstance.set(declaration, {
...universeConfig,
init: undefined,
overloads: undefined,
overloads: [],
});
}

if (checkCollect) {
checkReset.push([declaration, ngMocksUniverse.configInstance.get(declaration)]);
}
};

const mockInstanceMember = <T>(
Expand All @@ -92,11 +115,17 @@ const mockInstanceMember = <T>(
const overloads = config.overloads || [];
overloads.push([name, stub, encapsulation]);
config.overloads = overloads;
ngMocksUniverse.configInstance.set(declaration, config);
ngMocksUniverse.configInstance.set(declaration, {
...config,
});
const mockInstances = currentStack.mockInstance ?? [];
mockInstances.push(declaration);
currentStack.mockInstance = mockInstances;

if (checkCollect) {
checkReset.push([declaration, ngMocksUniverse.configInstance.get(declaration)]);
}

return stub;
};

Expand All @@ -121,7 +150,7 @@ export interface MockInstance {
*
* @see https://ng-mocks.sudo.eu/api/MockInstance#scope
*/
scope(scope?: 'each'): void;
scope(scope?: 'all'): void;
}

/**
Expand Down Expand Up @@ -202,13 +231,13 @@ export function MockInstance<T>(declaration: Type<T> | AbstractType<T> | Injecti

MockInstance.remember = () => ngMocksStack.stackPush();
MockInstance.restore = () => ngMocksStack.stackPop();
MockInstance.scope = (scope?: 'each') => {
if (scope === 'each') {
beforeEach(MockInstance.remember);
afterEach(MockInstance.restore);
} else {
MockInstance.scope = (scope?: 'all') => {
if (scope === 'all') {
beforeAll(MockInstance.remember);
afterAll(MockInstance.restore);
} else {
beforeEach(MockInstance.remember);
afterEach(MockInstance.restore);
}
};

Expand Down
2 changes: 1 addition & 1 deletion libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ const handleFixtureError = (e: any) => {
throw error;
};

const globalFlags = ngMocksUniverse.global.get('flags');
const flushTestBed = (flags: Record<string, any>): void => {
const globalFlags = ngMocksUniverse.global.get('flags');
const testBed: any = getTestBed();
if (flags.reset || (!testBed._instantiated && !testBed._testModuleRef)) {
ngMocks.flushTestBed();
Expand Down
4 changes: 2 additions & 2 deletions tests/mock-instance-scope/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class TargetService {
}

describe('mock-instance-scope', () => {
MockInstance.scope();
MockInstance.scope('all');
beforeAll(() =>
MockInstance(TargetService, () => ({
echo: () => 'beforeAll',
Expand All @@ -27,7 +27,7 @@ describe('mock-instance-scope', () => {
});

describe('nested', () => {
MockInstance.scope('each');
MockInstance.scope();

beforeEach(() =>
MockInstance(TargetService, () => ({
Expand Down

0 comments on commit 3e35252

Please sign in to comment.