Skip to content

Commit

Permalink
feat(faster): supports MockRender in beforeAll #488
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed May 8, 2021
1 parent 111d3c9 commit df4418c
Show file tree
Hide file tree
Showing 7 changed files with 428 additions and 75 deletions.
61 changes: 48 additions & 13 deletions docs/articles/api/ngMocks/faster.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ There is a `ngMocks.faster` feature that optimizes setup of similar test modules
and reduces required time on their execution.

Imagine a situation when `beforeEach` creates the same setup used by dozens of `it`.
This is the case where `ngMocks.faster` might be useful, simply call it before `beforeEach` and
This is the case where `ngMocks.faster` might be useful, simply call it before `beforeAll` and
**the Angular tests will run faster**.

```ts
describe('performance:correct', () => {
ngMocks.faster(); // <-- add it before

// The TestBed is not going to be changed between tests.
beforeEach(() => {
beforeAll(() => {
return MockBuilder(TargetComponent, TargetModule).keep(TargetService);
});

Expand All @@ -34,11 +34,10 @@ describe('performance:correct', () => {
If a test creates spies in `beforeEach` then this should be tuned,
because `ngMocks.faster` will detect this difference and display a notice.

A possible solution is usage of [MockInstance](../MockInstance.md) or to move creation of spies
outside of `beforeEach`.
A possible solution is usage of [MockInstance](../MockInstance.md) instead of manual declaration,
or to move creation of spies outside of `beforeEach`.

<details><summary>Click to see <strong>an example of MockInstance</strong></summary>
<p>
## Example of MockInstance

```ts
describe('beforeEach:mock-instance', () => {
Expand Down Expand Up @@ -66,11 +65,7 @@ describe('beforeEach:mock-instance', () => {
});
```

</p>
</details>

<details><summary>Click to see <strong>an example of optimizing spies in beforeEach</strong></summary>
<p>
## Example of optimizing spies in beforeEach

```ts
describe('beforeEach:manual-spy', () => {
Expand Down Expand Up @@ -101,5 +96,45 @@ describe('beforeEach:manual-spy', () => {
});
```

</p>
</details>
## MockRender

Usage of `ngMocks.faster()` covers [`MockRender`](../MockRender.md) too.

With its help, `MockRender` can be called in either `beforeEach` or `beforeAll`.
`beforeAll` won't reset its fixture after a test, and the fixture can be used in the next test.
Please pay attention that state of components also stays the same.

```ts
describe('issue-488:faster', () => {
let fixture: MockedComponentFixture<MyComponent>;

ngMocks.faster();

beforeAll(() => MockBuilder(MyComponent, MyModule));
beforeAll(() => fixture = MockRender(MyComponent));

it('first test has initial render', () => {
expect(ngMocks.formatText(fixture)).toEqual('1');

fixture.point.componentInstance.value += 1;
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('2');

fixture.point.componentInstance.reset();
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('0');
});

it('second test continues the prev state', () => {
expect(ngMocks.formatText(fixture)).toEqual('0');

fixture.point.componentInstance.value += 1;
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('1');

fixture.point.componentInstance.reset();
fixture.detectChanges();
expect(ngMocks.formatText(fixture)).toEqual('0');
});
});
```
99 changes: 99 additions & 0 deletions libs/ng-mocks/src/lib/common/ng-mocks-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { mapValues } from './core.helpers';
import ngMocksUniverse from './ng-mocks-universe';

export interface NgMocksStack {
id: object;
mockInstance?: any[];
}

type NgMocksStackCallback = (state: NgMocksStack, stack: NgMocksStack[]) => void;

// istanbul ignore next
const stack: NgMocksStack[] = ngMocksUniverse.global.get('reporter-stack') ?? [];
ngMocksUniverse.global.set('reporter-stack', stack);

// istanbul ignore next
const listenersPush: Set<NgMocksStackCallback> = ngMocksUniverse.global.get('reporter-stack-push') ?? new Set();
ngMocksUniverse.global.set('reporter-stack-push', listenersPush);

// istanbul ignore next
const listenersPop: Set<NgMocksStackCallback> = ngMocksUniverse.global.get('reporter-stack-pop') ?? new Set();
ngMocksUniverse.global.set('reporter-stack-pop', listenersPop);

const stackPush = () => {
const id = {};
ngMocksUniverse.global.set('reporter-stack-id', id);
const state = { id };
stack.push(state);

for (const callback of mapValues(listenersPush)) {
callback(state, stack);
}
};
const stackPop = () => {
const state = stack.pop();
// istanbul ignore if
if (stack.length === 0) {
const id = {};
stack.push({ id });
}

// istanbul ignore else
if (state) {
for (const callback of mapValues(listenersPop)) {
callback(state, stack);
}
}

ngMocksUniverse.global.set('reporter-stack-id', stack[stack.length - 1].id);
};

const reporterStack: jasmine.CustomReporter = {
jasmineDone: stackPop,
jasmineStarted: stackPush,
specDone: stackPop,
specStarted: stackPush,
suiteDone: stackPop,
suiteStarted: stackPush,
};

const install = () => {
if (!ngMocksUniverse.global.has('reporter-stack-install')) {
jasmine.getEnv().addReporter(reporterStack);
ngMocksUniverse.global.set('reporter-stack-install', true);
stackPush();
}

return ngMocksUniverse.global.has('reporter-stack-install');
};

// istanbul ignore next
const subscribePush = (callback: NgMocksStackCallback) => {
listenersPush.add(callback);
if (stack.length) {
callback(stack[stack.length - 1], stack);
}
};

// istanbul ignore next
const subscribePop = (callback: NgMocksStackCallback) => {
listenersPop.add(callback);
};

// istanbul ignore next
const unsubscribePush = (callback: NgMocksStackCallback) => {
listenersPush.delete(callback);
};

// istanbul ignore next
const unsubscribePop = (callback: NgMocksStackCallback) => {
listenersPop.delete(callback);
};

export default {
install,
subscribePop,
subscribePush,
unsubscribePop,
unsubscribePush,
};
29 changes: 21 additions & 8 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.faster.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import { getTestBed, TestBed } from '@angular/core/testing';
import { ComponentFixture, getTestBed, TestBed } from '@angular/core/testing';

import ngMocksStack, { NgMocksStack } from '../common/ng-mocks-stack';
import ngMocksUniverse from '../common/ng-mocks-universe';

import mockHelperFlushTestBed from './mock-helper.flush-test-bed';

const resetFixtures = (stack: NgMocksStack) => {
const activeFixtures: Array<ComponentFixture<any> & { ngMocksStackId?: any }> =
(getTestBed() as any)._activeFixtures || /* istanbul ignore next */ [];

for (let i = activeFixtures.length - 1; i >= 0; i -= 1) {
if (!activeFixtures[i].ngMocksStackId || activeFixtures[i].ngMocksStackId === stack.id) {
activeFixtures[i].destroy();
activeFixtures.splice(i, 1);
}
}
if (activeFixtures.length === 0) {
mockHelperFlushTestBed();
}
};

export default () => {
ngMocksStack.install();

beforeAll(() => {
if (ngMocksUniverse.global.has('bullet:customized')) {
TestBed.resetTestingModule();
}
ngMocksUniverse.global.set('bullet', true);
});

afterEach(() => {
mockHelperFlushTestBed();
for (const fixture of (getTestBed() as any)._activeFixtures || /* istanbul ignore next */ []) {
fixture.destroy();
}
ngMocksStack.subscribePop(resetFixtures);
});

afterAll(() => {
ngMocksStack.unsubscribePop(resetFixtures);
ngMocksUniverse.global.delete('bullet');
if (ngMocksUniverse.global.has('bullet:reset')) {
TestBed.resetTestingModule();
Expand Down
86 changes: 33 additions & 53 deletions libs/ng-mocks/src/lib/mock-instance/mock-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,42 @@ import { InjectionToken, Injector } from '@angular/core';

import { AbstractType, Type } from '../common/core.types';
import funcImportExists from '../common/func.import-exists';
import ngMocksStack, { NgMocksStack } from '../common/ng-mocks-stack';
import ngMocksUniverse from '../common/ng-mocks-universe';

const stack: any[][] = [[]];
const stackPush = () => {
stack.push([]);
};
const stackPop = () => {
for (const declaration of stack.pop() || /* istanbul ignore next */ []) {
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();
}
// istanbul ignore if
if (stack.length === 0) {
stack.push([]);
currentStack = stack[stack.length - 1];
});

ngMocksStack.subscribePush(() => {
// On start we have to flush any caches,
// they are not from this spec.
const set = ngMocksUniverse.getLocalMocks();
set.splice(0, set.length);
});
ngMocksStack.subscribePop(() => {
const set = ngMocksUniverse.getLocalMocks();
while (set.length) {
const [declaration, config] = set.pop() || /* istanbul ignore next */ [];
const universeConfig = ngMocksUniverse.configInstance.has(declaration)
? ngMocksUniverse.configInstance.get(declaration)
: {};
ngMocksUniverse.configInstance.set(declaration, {
...universeConfig,
...config,
});
}
};

const reporterStack: jasmine.CustomReporter = {
jasmineDone: stackPop,
jasmineStarted: stackPush,
specDone: stackPop,
specStarted: stackPush,
suiteDone: stackPop,
suiteStarted: stackPush,
};
});

const reporter: jasmine.CustomReporter = {
specDone: () => {
const set = ngMocksUniverse.getLocalMocks();
while (set.length) {
const [declaration, config] = set.pop() || /* istanbul ignore next */ [];
const universeConfig = ngMocksUniverse.configInstance.has(declaration)
? ngMocksUniverse.configInstance.get(declaration)
: {};
ngMocksUniverse.configInstance.set(declaration, {
...universeConfig,
...config,
});
}
},
specStarted: () => {
// On start we have to flush any caches,
// they are not from this spec.
const set = ngMocksUniverse.getLocalMocks();
set.splice(0, set.length);
},
};

let installReporter = true;
const restore = (declaration: any, config: any): void => {
if (installReporter) {
jasmine.getEnv().addReporter(reporter);
installReporter = false;
}

ngMocksStack.install();
ngMocksUniverse.getLocalMocks().push([declaration, config]);
};

Expand Down Expand Up @@ -101,23 +83,21 @@ const mockInstanceConfig = <T>(declaration: Type<T> | AbstractType<T> | Injectio
}
};

let installStackReporter = true;
const mockInstanceMember = <T>(
declaration: Type<T> | AbstractType<T> | InjectionToken<T>,
name: string,
stub: any,
encapsulation?: 'get' | 'set',
) => {
if (installStackReporter) {
jasmine.getEnv().addReporter(reporterStack);
installStackReporter = false;
}
ngMocksStack.install();
const config = ngMocksUniverse.configInstance.has(declaration) ? ngMocksUniverse.configInstance.get(declaration) : {};
const overloads = config.overloads || [];
overloads.push([name, stub, encapsulation]);
config.overloads = overloads;
ngMocksUniverse.configInstance.set(declaration, config);
stack[stack.length - 1].push(declaration);
const mockInstances = currentStack.mockInstance ?? [];
mockInstances.push(declaration);
currentStack.mockInstance = mockInstances;

return stub;
};
Expand Down
6 changes: 5 additions & 1 deletion libs/ng-mocks/src/lib/mock-render/mock-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import coreDefineProperty from '../common/core.define-property';
import { Type } from '../common/core.types';
import funcImportExists from '../common/func.import-exists';
import { isNgDef } from '../common/func.is-ng-def';
import ngMocksUniverse from '../common/ng-mocks-universe';
import { ngMocks } from '../mock-helper/mock-helper';
import { MockService } from '../mock-service/mock-service';

Expand All @@ -25,7 +26,10 @@ const generateFixture = ({ params, options }: any) => {
declarations: [MockRenderComponent],
});

return TestBed.createComponent(MockRenderComponent);
const fixture = TestBed.createComponent(MockRenderComponent);
coreDefineProperty(fixture, 'ngMocksStackId', ngMocksUniverse.global.get('reporter-stack-id'));

return fixture;
};

const fixtureFactory = <T>(template: any, meta: Directive, params: any, flags: any): ComponentFixture<T> => {
Expand Down
Loading

0 comments on commit df4418c

Please sign in to comment.