Skip to content

Commit

Permalink
feat: extending mocked things via MockInstance
Browse files Browse the repository at this point in the history
closes #170
  • Loading branch information
satanTime committed Jul 19, 2020
1 parent 3d6b8b4 commit 1ab2c9d
Show file tree
Hide file tree
Showing 14 changed files with 491 additions and 121 deletions.
112 changes: 101 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,20 @@ Our tests:

### Sections:

- [MockComponent](#mockcomponents)
- [MockDirective](#mockdirectives)
- [MockPipe](#mockpipes)
- [MockDeclaration](#mockdeclarations)
- [MockModule](#mockmodule)
- [MockBuilder](#mockbuilder) - facilitates creation of a mocked environment
- [MockRender](#mockrender) - facilitates rendering of components
- [MockInstance](#mockinstance) - customizes mocked instances on an early stage
- [ngMocks](#ngmocks) - facilitates interaction with a fixture, DOM etc.

* [MockBuilder](#mockbuilder) - facilitate creation of a mocked environment
* [MockRender](#mockrender) - facilitate render of components
* [ngMocks](#ngmocks) - facilitate extraction of directives of an element
* [MockComponent](#mockcomponents)
* [MockDirective](#mockdirectives)
* [MockPipe](#mockpipes)
* [MockDeclaration](#mockdeclarations)
* [MockModule](#mockmodule)

- [Reactive Forms Components](#mocked-reactive-forms-components)
- [Structural Components](#usage-example-of-structural-directives)
- [Auto Spy](#auto-spy)
* [Reactive Forms Components](#mocked-reactive-forms-components)
* [Structural Components](#usage-example-of-structural-directives)
* [Auto Spy](#auto-spy)

---

Expand Down Expand Up @@ -800,6 +801,95 @@ describe('MockRender', () => {

---

## MockInstance

`MockInstance` is useful when you want to configure spies of a component before it has been rendered.

MockInstance supports: Modules, Components, Directives, Pipes and Services.

```typescript
MockInstance(MyComponent, {
init: (instance: MyComponent, injector: Injector): void => {
// Now you can customize a mocked instance of MyComponent.
// If you use auto-spy then all methods have been spied already here.
},
});
```

After a test you can reset changes to avoid their influence in other tests via a call of `MockReset()`.

<details><summary>Click to see <strong>a usage example</strong></summary>
<p>

```typescript
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MockBuilder, MockInstance, MockRender, MockReset } from 'ng-mocks';
import { staticFalse } from 'ng-mocks/dist/tests';
import { EMPTY, Observable, Subject } from 'rxjs';

// A child component that contains update$ the parent component wants to listen to.
@Component({
selector: 'target',
template: '{{ update$ | async }}',
})
export class TargetComponent {
public update$: Observable<void>;

constructor() {
const subject = new Subject<void>();
this.update$ = subject;
subject.complete();
}
}

// A parent component that uses @ViewChild to listen to update$ of its child component.
@Component({
selector: 'real',
template: '<target></target>',
})
export class RealComponent implements AfterViewInit {
@ViewChild(TargetComponent, { ...staticFalse }) public child: TargetComponent;

ngAfterViewInit() {
this.child.update$.subscribe();
}
}

describe('MockInstance', () => {
// A normal setup of the TestBed, TargetComponent will be mocked.
beforeEach(() => MockBuilder(RealComponent).mock(TargetComponent));

beforeEach(() => {
// Because TargetComponent is mocked its update$ is undefined and
// ngAfterViewInit of the parent component will fail on .subscribe().
// Let's fix it via defining custom initialization of the mock.
MockInstance(TargetComponent, {
init: (instance, injector) => {
instance.update$ = EMPTY; // comment this line to check the failure.
// if you want you can use injector.get(Service) for a more complicated initialization.
},
});
});

// Don't forget to reset MockInstance back.
afterEach(MockReset);

it('should render', () => {
// Without the custom initialization rendering would fail here with
// "Cannot read property 'subscribe' of undefined"
const fixture = MockRender(RealComponent);

// Let's check that the mocked component has been decorated by the custom initialization.
expect(fixture.point.componentInstance.child.update$).toBe(EMPTY);
});
});
```

</p>
</details>

---

## ngMocks

ngMocks provides functions to get attribute and structural directives from an element, find components and mock objects.
Expand Down
65 changes: 65 additions & 0 deletions examples/MockInstance/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MockBuilder, MockInstance, MockRender, MockReset } from 'ng-mocks';
import { Observable, Subject } from 'rxjs';

// A child component that contains update$ the parent component wants to listen to.
@Component({
selector: 'target',
template: '{{ update$ | async }}',
})
export class TargetComponent {
public update$: Observable<void>;

constructor() {
const subject = new Subject<void>();
this.update$ = subject;
subject.complete();
}
}

// A parent component that uses @ViewChild to listen to update$ of its child component.
@Component({
selector: 'real',
template: '<target></target>',
})
export class RealComponent implements AfterViewInit {
@ViewChild(TargetComponent, {
static: false,
} as any)
public child: TargetComponent;

ngAfterViewInit() {
this.child.update$.subscribe();
}
}

describe('MockInstance', () => {
// A normal setup of the TestBed, TargetComponent will be mocked.
beforeEach(() => MockBuilder(RealComponent).mock(TargetComponent));

beforeEach(() => {
// Because TargetComponent is mocked its update$ is undefined and
// ngAfterViewInit of the parent component will fail on .subscribe().
// Let's fix it via defining custom initialization of the mock.
MockInstance(TargetComponent, {
init: (instance, injector) => {
const subject = new Subject<void>();
subject.complete();
instance.update$ = subject; // comment this line to check the failure.
// if you want you can use injector.get(Service) for a more complicated initialization.
},
});
});

// Don't forget to reset MockInstance back.
afterEach(MockReset);

it('should render', () => {
// Without the custom initialization rendering would fail here with
// "Cannot read property 'subscribe' of undefined"
const fixture = MockRender(RealComponent);

// Let's check that the mocked component has been decorated by the custom initialization.
expect(fixture.point.componentInstance.child.update$).toBeDefined();
});
});
20 changes: 12 additions & 8 deletions examples/NG_MOCKS/NG_MOCKS.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,22 +94,26 @@ describe('NG_MOCKS:deep', () => {
expect(isMockedNgDefOf(pipeWeWantToMock, PipeWeWantToMock, 'p')).toBeTruthy();
const serviceWeWantToMock = mocks.get(ServiceWeWantToMock);
expect(serviceWeWantToMock).toBeDefined();
expect(serviceWeWantToMock.useValue).toBeDefined();
expect(serviceWeWantToMock.useValue.getName).toBeDefined();
expect(serviceWeWantToMock.useValue.getName()).toBeUndefined();
expect(serviceWeWantToMock.useFactory).toBeDefined();
const serviceWeWantToMockInstance = serviceWeWantToMock.useFactory();
expect(serviceWeWantToMockInstance.getName).toBeDefined();
expect(serviceWeWantToMockInstance.getName()).toBeUndefined();
expect(mocks.has(INJECTION_TOKEN_WE_WANT_TO_MOCK)).toBeDefined();
expect(mocks.get(INJECTION_TOKEN_WE_WANT_TO_MOCK)).toBeUndefined();

// customize
const serviceWeWantToCustomize = mocks.get(ServiceWeWantToCustomize);
expect(serviceWeWantToCustomize).toBeDefined();
expect(serviceWeWantToCustomize.useValue).toBeDefined();
expect(serviceWeWantToCustomize.useValue.getName).toBeDefined();
expect(serviceWeWantToCustomize.useValue.getName()).toEqual('My Customized String');
expect(serviceWeWantToCustomize.useValue.prop1).toEqual(true);
expect(serviceWeWantToCustomize.useFactory).toBeDefined();
const serviceWeWantToCustomizeInstance = serviceWeWantToCustomize.useFactory();
expect(serviceWeWantToCustomizeInstance.getName).toBeDefined();
expect(serviceWeWantToCustomizeInstance.getName()).toEqual('My Customized String');
expect(serviceWeWantToCustomizeInstance.prop1).toEqual(true);
const injectionTokenWeWantToCustomize = mocks.get(INJECTION_TOKEN_WE_WANT_TO_CUSTOMIZE);
expect(injectionTokenWeWantToCustomize).toBeDefined();
expect(injectionTokenWeWantToCustomize.useValue).toEqual('My_Token');
expect(injectionTokenWeWantToCustomize.useFactory).toBeDefined();
const injectionTokenWeWantToCustomizeInstance = injectionTokenWeWantToCustomize.useFactory();
expect(injectionTokenWeWantToCustomizeInstance).toEqual('My_Token');

// restore
const pipeWeWantToRestore = mocks.get(PipeWeWantToRestore);
Expand Down
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './lib/mock-component';
export * from './lib/mock-declaration';
export * from './lib/mock-directive';
export * from './lib/mock-helper';
export * from './lib/mock-instance';
export * from './lib/mock-module';
export * from './lib/mock-pipe';
export * from './lib/mock-render';
Expand Down
50 changes: 43 additions & 7 deletions lib/common/Mock.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
import { EventEmitter } from '@angular/core';
import { AbstractControl, ControlValueAccessor, ValidationErrors, Validator } from '@angular/forms';
import { EventEmitter, Injector, Optional } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NgControl, ValidationErrors, Validator } from '@angular/forms';

import { mockServiceHelper } from '../mock-service';

import { ngMocksUniverse } from './ng-mocks-universe';

// tslint:disable-next-line:interface-over-type-literal
export type ngMocksMockConfig = {
outputs?: string[];
setNgValueAccessor?: boolean;
};

// tslint:disable-next-line:no-unnecessary-class
export class Mock {
// tslint:disable-next-line:variable-name
public readonly __ngMocksMock: true = true;

constructor() {
// tslint:disable-next-line:variable-name
protected readonly __ngMocksConfig?: ngMocksMockConfig;

constructor(@Optional() injector?: Injector) {
const mockOf = (this.constructor as any).mockOf;

if (injector && this.__ngMocksConfig && this.__ngMocksConfig.setNgValueAccessor) {
try {
// tslint:disable-next-line:no-bitwise
const ngControl = (injector.get as any)(/* A5 */ NgControl, undefined, 0b1010);
if (ngControl && !ngControl.valueAccessor) {
ngControl.valueAccessor = this;
}
} catch (e) {
// nothing to do.
}
}

// setting outputs
for (const output of (this as any).__mockedOutputs) {

const mockedOutputs = [];
for (const output of this.__ngMocksConfig && this.__ngMocksConfig.outputs ? this.__ngMocksConfig.outputs : []) {
mockedOutputs.push(output.split(':')[0]);
}

for (const output of mockedOutputs) {
if ((this as any)[output] || Object.getOwnPropertyDescriptor(this, output)) {
continue;
}
Expand All @@ -34,13 +65,13 @@ export class Mock {
}

// setting mocks for original class methods and props
for (const method of mockServiceHelper.extractMethodsFromPrototype((this.constructor as any).mockOf.prototype)) {
for (const method of mockServiceHelper.extractMethodsFromPrototype(mockOf.prototype)) {
if ((this as any)[method] || Object.getOwnPropertyDescriptor(this, method)) {
continue;
}
mockServiceHelper.mock(this, method);
}
for (const prop of mockServiceHelper.extractPropertiesFromPrototype((this.constructor as any).mockOf.prototype)) {
for (const prop of mockServiceHelper.extractPropertiesFromPrototype(mockOf.prototype)) {
if ((this as any)[prop] || Object.getOwnPropertyDescriptor(this, prop)) {
continue;
}
Expand All @@ -49,7 +80,12 @@ export class Mock {
}

// and faking prototype
Object.setPrototypeOf(this, (this.constructor as any).mockOf.prototype);
Object.setPrototypeOf(this, mockOf.prototype);

const config = ngMocksUniverse.config.get(mockOf);
if (config && config.init && config.init) {
config.init(this, injector);
}
}
}

Expand Down
9 changes: 3 additions & 6 deletions lib/common/mock-of.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Type } from './lib';
import { ngMocksMockConfig } from './Mock';

// This helps with debugging in the browser. Decorating mock classes with this
// will change the display-name of the class to 'MockOf-<ClassName>` so our
Expand All @@ -7,16 +8,12 @@ import { Type } from './lib';
// Additionally, if we set breakpoints, we can inspect the actual class being mocked
// by looking into the 'mockOf' property on the class.
/* tslint:disable-next-line variable-name */
export const MockOf = (mockClass: Type<any>, outputs?: string[]) => (constructor: Type<any>) => {
export const MockOf = (mockClass: Type<any>, config?: ngMocksMockConfig) => (constructor: Type<any>) => {
Object.defineProperties(constructor, {
mockOf: { value: mockClass },
name: { value: `MockOf${mockClass.name}` },
nameConstructor: { value: constructor.name },
});

const mockedOutputs = [];
for (const output of outputs || []) {
mockedOutputs.push(output.split(':')[0]);
}
constructor.prototype.__mockedOutputs = mockedOutputs;
constructor.prototype.__ngMocksConfig = config;
};
11 changes: 8 additions & 3 deletions lib/mock-builder/mock-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { MockComponent } from '../mock-component';
import { MockDirective } from '../mock-directive';
import { MockModule, MockProvider } from '../mock-module';
import { MockPipe } from '../mock-pipe';
import { mockServiceHelper } from '../mock-service';

export interface IMockBuilderResult {
testBed: typeof TestBed;
Expand Down Expand Up @@ -150,7 +151,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
// mocking requested things.
for (const def of mapValues(this.mockDef.provider)) {
if (this.mockDef.providerMock.has(def)) {
ngMocksUniverse.builder.set(def, { provide: def, useValue: this.mockDef.providerMock.get(def) });
ngMocksUniverse.builder.set(def, mockServiceHelper.useFactory(def, this.mockDef.providerMock.get(def)));
} else {
ngMocksUniverse.builder.set(def, MockProvider(def));
}
Expand Down Expand Up @@ -324,8 +325,12 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
return this;
}

public mock(pipe: Type<PipeTransform>, config?: IMockBuilderConfig): this;
public mock(pipe: Type<PipeTransform>, mock?: PipeTransform['transform'], config?: IMockBuilderConfig): this;
public mock<T extends PipeTransform>(pipe: Type<T>, config?: IMockBuilderConfig): this;
public mock<T extends PipeTransform>(
pipe: Type<T>,
mock?: PipeTransform['transform'],
config?: IMockBuilderConfig
): this;
public mock<T>(token: InjectionToken<T>, mock?: any): this;
public mock<T>(def: Type<T>, mock: IMockBuilderConfig): this;
public mock<T>(provider: Type<T>, mock?: any): this;
Expand Down
Loading

0 comments on commit 1ab2c9d

Please sign in to comment.