Skip to content

Commit

Permalink
fix(mock-render): providing a MockRenderFactory in order to reuse the…
Browse files Browse the repository at this point in the history
… same middleware component
  • Loading branch information
satanTime committed May 19, 2021
1 parent 6948031 commit 79fa336
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 135 deletions.
2 changes: 1 addition & 1 deletion .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ checks:
enabled: false
similar-code:
config:
threshold: 70
threshold: 100
38 changes: 37 additions & 1 deletion docs/articles/api/MockRender.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,6 @@ expect(params.o1).toEqual(1);
expect(params.o2).toHaveBeenCalledWith(2);
```


## Example with a component

```ts
Expand Down Expand Up @@ -369,6 +368,43 @@ const fixture = MockRender(
);
```

## Factory

Because `MockRender` creates a middleware component, it can add an undesired impact on test performance.
Especially, in cases, when the same setup should be used in different tests.

For example, we have 5 tests and every test calls `MockRender(MyComponent)`.
It means that every time a middleware component has been created and injected into `TestBed`,
whereas `MockRender` could have reused the existing middleware component and simply to create a new fixture out of it.

In such situations, `MockRenderFactory` can be used instead of `MockRender`.
It has the same interface as `MockRender`, but instead of an instant render,
it returns a factory function. The factory function simply creates a new fixture out of its middleware component.

Considering the conditions above, we would need to create a factory once with help of `MockRenderFactory` in `beforeAll`,
and then 5 tests should call the factory in order to create fixtures.

```ts
describe('Maximum reusage', () => {
ngMocks.faster();

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

let factory: MockRenderFactory<MyComponent>;
beforeAll(() => factory = MockRenderFactory(MyComponent));

it('covers one case', () => {
const fixture = factory();
expect(fixture.point.componentInstance.input1).toEqual(1);
});

it('covers another case', () => {
const fixture = factory();
expect(fixture.point.componentInstance.input2).toEqual(2);
});
});
```

## Advanced example

:::tip
Expand Down
1 change: 1 addition & 0 deletions libs/ng-mocks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export * from './lib/mock-pipe/types';
export * from './lib/mock-provider/mock-provider';

export * from './lib/mock-render/mock-render';
export * from './lib/mock-render/mock-render-factory';
export * from './lib/mock-render/types';

export { registerMockFunction } from './lib/mock-service/helper.mock-service';
Expand Down
240 changes: 240 additions & 0 deletions libs/ng-mocks/src/lib/mock-render/mock-render-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
import { Component, DebugElement, Directive, InjectionToken } from '@angular/core';
import { getTestBed, TestBed } from '@angular/core/testing';

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 helperDefinePropertyDescriptor from '../mock-service/helper.define-property-descriptor';
import { MockService } from '../mock-service/mock-service';

import funcGenerateTemplate from './func.generate-template';
import funcInstallPropReader from './func.install-prop-reader';
import funcReflectTemplate from './func.reflect-template';
import { DefaultRenderComponent, IMockRenderOptions, MockedComponentFixture } from './types';

interface MockRenderFactory<C = any, F = DefaultRenderComponent<C>> {
declaration: Type<never>;
params: F;
(): MockedComponentFixture<C, F>;
}

const generateWrapper = ({ params, options, inputs }: any) => {
class MockRenderComponent {
public constructor() {
if (!params) {
for (const input of inputs || []) {
let value: any = null;
helperDefinePropertyDescriptor(this, input, {
get: () => value,
set: (newValue: any) => (value = newValue),
});
}
}
funcInstallPropReader(this, params);
}
}

Component(options)(MockRenderComponent);
TestBed.configureTestingModule({
declarations: [MockRenderComponent],
});

return MockRenderComponent;
};

const createWrapper = (template: any, meta: Directive, params: any, flags: any): Type<any> => {
const mockTemplate = funcGenerateTemplate(template, { ...meta, params });
const options: Component = {
providers: flags.providers,
selector: 'mock-render',
template: mockTemplate,
};

return generateWrapper({ ...meta, params, options });
};

const isExpectedRender = (template: any): boolean =>
typeof template === 'string' || isNgDef(template, 'c') || isNgDef(template, 'd');

const renderDeclaration = (fixture: any, template: any, params: any): void => {
fixture.point = fixture.debugElement.children[0] || fixture.debugElement.childNodes[0];
if (isNgDef(template, 'd')) {
helperDefinePropertyDescriptor(fixture.point, 'componentInstance', {
get: () => ngMocks.get(fixture.point, template),
});
}
tryWhen(!params, () => funcInstallPropReader(fixture.componentInstance, fixture.point?.componentInstance));
};

const renderInjection = (fixture: any, template: any, params: any): void => {
const instance = TestBed.get(template);
if (params) {
ngMocks.stub(instance, params);
}
fixture.point = MockService(DebugElement, {
childNodes: [],
children: [],
componentInstance: instance,
nativeElement: MockService(HTMLElement),
});
funcInstallPropReader(fixture.componentInstance, fixture.point.componentInstance, true);
};

const tryWhen = (flag: boolean, callback: () => void) => {
if (!flag) {
return;
}
try {
// ivy throws Error: Expecting instance of DOM Element
callback();
} catch (e) {
// nothing to do
}
};

const handleFixtureError = (e: any) => {
const message = [
'Forgot to flush TestBed?',
'MockRender cannot be used without a reset after TestBed.get / TestBed.inject / TestBed.createComponent and another MockRender in the same test.',
'To flush TestBed, add a call of ngMocks.flushTestBed() before the call of MockRender, or pass `reset: true` to MockRender options.',
'If you want to mock a service before rendering, consider usage of MockInstance.',
].join(' ');
const error = new Error(message);
coreDefineProperty(error, 'parent', e, false);
throw error;
};

const generateFactory = (componentCtor: Type<any>, flags: any, params: any, template: any) => {
const result = () => {
const fixture: any = TestBed.createComponent(componentCtor);
coreDefineProperty(fixture, 'ngMocksStackId', ngMocksUniverse.global.get('reporter-stack-id'));

if (flags.detectChanges === undefined || flags.detectChanges) {
fixture.detectChanges();
}

if (isExpectedRender(template)) {
renderDeclaration(fixture, template, params);
} else {
renderInjection(fixture, template, params);
}

return fixture;
};
result.declaration = componentCtor;
result.params = params;

return result;
};

/**
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory<MComponent>(
template: InjectionToken<MComponent>,
params?: undefined | null,
detectChangesOrOptions?: boolean | IMockRenderOptions,
): MockRenderFactory<MComponent, void>;

/**
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory<MComponent>(
template: Type<MComponent>,
params: undefined | null,
detectChangesOrOptions?: boolean | IMockRenderOptions,
): MockRenderFactory<MComponent, MComponent>;

/**
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory<MComponent, TComponent extends object>(
template: Type<MComponent>,
params: TComponent,
detectChangesOrOptions?: boolean | IMockRenderOptions,
): MockRenderFactory<MComponent, TComponent>;

/**
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory<MComponent, TComponent extends object = Record<keyof any, any>>(
template: Type<MComponent>,
params: TComponent,
detectChangesOrOptions?: boolean | IMockRenderOptions,
): MockRenderFactory<MComponent, TComponent>;

/**
* Without params we should not autocomplete any keys of any types.
*
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory<MComponent>(template: Type<MComponent>): MockRenderFactory<MComponent, MComponent>;

/**
* An empty string does not have point.
*
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory(template: ''): MockRenderFactory<void, undefined>;

/**
* Without params we should not autocomplete any keys of any types.
*
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory<MComponent = void>(template: string): MockRenderFactory<MComponent>;

/**
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory<MComponent = void>(
template: string,
params: undefined | null,
detectChangesOrOptions?: boolean | IMockRenderOptions,
): MockRenderFactory<MComponent, void>;

/**
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory<MComponent = void, TComponent extends Record<keyof any, any> = Record<keyof any, any>>(
template: string,
params: TComponent,
detectChangesOrOptions?: boolean | IMockRenderOptions,
): MockRenderFactory<MComponent, TComponent>;

/**
* @see https://ng-mocks.sudo.eu/api/MockRender#factory
*/
function MockRenderFactory<MComponent, TComponent extends Record<keyof any, any> = Record<keyof any, any>>(
template: string,
params: TComponent,
detectChangesOrOptions?: boolean | IMockRenderOptions,
): MockRenderFactory<MComponent, TComponent>;

function MockRenderFactory<MComponent, TComponent extends Record<keyof any, any>>(
template: string | Type<MComponent> | InjectionToken<MComponent>,
params?: TComponent,
flags: boolean | IMockRenderOptions = true,
): any {
funcImportExists(template, 'MockRender');

const flagsObject: IMockRenderOptions = typeof flags === 'boolean' ? { detectChanges: flags } : { ...flags };
const meta: Directive = typeof template === 'string' || isNgDef(template, 't') ? {} : funcReflectTemplate(template);

const testBed: any = getTestBed();
if (flagsObject.reset || (!testBed._instantiated && !testBed._testModuleRef)) {
ngMocks.flushTestBed();
}
try {
const componentCtor: any = createWrapper(template, meta, params, flagsObject);

return generateFactory(componentCtor, flagsObject, params, template);
} catch (e) {
handleFixtureError(e);
}
}

export { MockRenderFactory };
Loading

0 comments on commit 79fa336

Please sign in to comment.