From 52945fd263d3abb2455b467f28548daaddeced70 Mon Sep 17 00:00:00 2001 From: Michael Gusev Date: Sat, 28 Mar 2020 22:37:16 +0100 Subject: [PATCH] feat: support to inject a library-related service mocker Closes #87 --- README.md | 11 ++++ e2e/spies/fixtures.components.ts | 19 +++++++ e2e/spies/fixtures.modules.ts | 10 ++++ e2e/spies/fixtures.providers.ts | 10 ++++ e2e/spies/test.spec.ts | 69 +++++++++++++++++++++++ karma.conf.ts | 1 + lib/common/Mock.ts | 2 +- lib/mock-component/mock-component.spec.ts | 2 +- lib/mock-directive/mock-directive.spec.ts | 2 +- lib/mock-service/mock-service.ts | 30 ++++++++-- mockery/jasmine.ts | 5 ++ mockery/jest.ts | 5 ++ tsconfig.json | 5 +- 13 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 e2e/spies/fixtures.components.ts create mode 100644 e2e/spies/fixtures.modules.ts create mode 100644 e2e/spies/fixtures.providers.ts create mode 100644 e2e/spies/test.spec.ts create mode 100644 mockery/jasmine.ts create mode 100644 mockery/jest.ts diff --git a/README.md b/README.md index a8d83641c5..87f30d11e7 100644 --- a/README.md +++ b/README.md @@ -442,6 +442,17 @@ returns first found attribute or structural directive which belongs to current e `MockHelper.findDirectives(fixture.debugElement, Directive)` returns all found attribute or structural directives which belong to current element and all its child. +## Auto Spy + +Add the next code to `src/test.ts` if you want all mocked methods and functions to be a jasmine spy. +```typescript +import 'ng-mocks/dist/mockery/jasmine'; +``` +In case of jest. +```typescript +import 'ng-mocks/dist/mockery/jest'; +``` + ## Other examples of tests More detailed examples can be found in diff --git a/e2e/spies/fixtures.components.ts b/e2e/spies/fixtures.components.ts new file mode 100644 index 0000000000..21dfbc0b96 --- /dev/null +++ b/e2e/spies/fixtures.components.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { TargetService } from './fixtures.providers'; + +@Component({ + selector: 'target', + template: '', +}) +export class TargetComponent { + protected service: TargetService; + + constructor(service: TargetService) { + this.service = service; + this.service.echo('constructor'); + } + + public echo(): string { + return this.service.echo('TargetComponent'); + } +} diff --git a/e2e/spies/fixtures.modules.ts b/e2e/spies/fixtures.modules.ts new file mode 100644 index 0000000000..784ec4d5b5 --- /dev/null +++ b/e2e/spies/fixtures.modules.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { TargetService } from './fixtures.providers'; + +@NgModule({ + providers: [ + TargetService, + ], +}) +export class TargetModule { +} diff --git a/e2e/spies/fixtures.providers.ts b/e2e/spies/fixtures.providers.ts new file mode 100644 index 0000000000..ed3362df27 --- /dev/null +++ b/e2e/spies/fixtures.providers.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class TargetService { + protected value = 'TargetService'; + + public echo(value?: string): string { + return value ? value : this.value; + } +} diff --git a/e2e/spies/test.spec.ts b/e2e/spies/test.spec.ts new file mode 100644 index 0000000000..30a26d51cc --- /dev/null +++ b/e2e/spies/test.spec.ts @@ -0,0 +1,69 @@ +import { inject, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MockModule, MockRender } from 'ng-mocks'; + +import { TargetComponent } from './fixtures.components'; +import { TargetModule } from './fixtures.modules'; +import { TargetService } from './fixtures.providers'; +import createSpyObj = jasmine.createSpyObj; +import Spy = jasmine.Spy; + +describe('spies:real', () => { + beforeEach(() => TestBed.configureTestingModule({ + declarations: [TargetComponent], + imports: [TargetModule], + }).compileComponents()); + + it('should render', () => { + const fixture = MockRender(TargetComponent); + const component = fixture.debugElement.query(By.directive(TargetComponent)).componentInstance as TargetComponent; + expect(component).toBeDefined(); + expect(component.echo()).toEqual('TargetComponent'); + }); +}); + +describe('spies:manual-mock', () => { + beforeEach(() => { + const spy = createSpyObj('TargetService', ['echo']); + spy.echo.and.returnValue('fake'); + + return TestBed.configureTestingModule({ + declarations: [TargetComponent], + imports: [MockModule(TargetModule)], + providers: [ + { + provide: TargetService, + useValue: spy, + } + ], + }).compileComponents(); + }); + + it('should get manually mocked service', inject([TargetService], (targetService: TargetService) => { + const fixture = MockRender(TargetComponent); + const component = fixture.debugElement.query(By.directive(TargetComponent)).componentInstance as TargetComponent; + expect(component).toBeDefined(); + expect(targetService.echo).toHaveBeenCalledTimes(1); + expect(targetService.echo).toHaveBeenCalledWith('constructor'); + expect(component.echo()).toEqual('fake'); + expect(targetService.echo).toHaveBeenCalledTimes(2); // tslint:disable-line:no-magic-numbers + })); +}); + +describe('spies:auto-mock', () => { + beforeEach(() => TestBed.configureTestingModule({ + declarations: [TargetComponent], + imports: [MockModule(TargetModule)], + }).compileComponents()); + + it('should get already mocked service', inject([TargetService], (targetService: TargetService) => { + const fixture = MockRender(TargetComponent); + const component = fixture.debugElement.query(By.directive(TargetComponent)).componentInstance as TargetComponent; + expect(component).toBeDefined(); + expect(targetService.echo).toHaveBeenCalledTimes(1); + expect(targetService.echo).toHaveBeenCalledWith('constructor'); + (targetService.echo as Spy).and.returnValue('faked'); + expect(component.echo()).toEqual('faked'); + expect(targetService.echo).toHaveBeenCalledTimes(2); // tslint:disable-line:no-magic-numbers + })); +}); diff --git a/karma.conf.ts b/karma.conf.ts index d51422cbc0..7347e3c238 100644 --- a/karma.conf.ts +++ b/karma.conf.ts @@ -29,6 +29,7 @@ module.exports = (config: any) => { 'node_modules/zone.js/dist/fake-async-test.js', 'karma-test-shim.ts', 'index.ts', + 'mockery/jasmine.ts', { pattern: 'lib/**/*.ts' }, { pattern: 'e2e/**/*.ts' }, { pattern: 'examples/**/*.ts' }, diff --git a/lib/common/Mock.ts b/lib/common/Mock.ts index 066cce297e..9be6f307a3 100644 --- a/lib/common/Mock.ts +++ b/lib/common/Mock.ts @@ -12,7 +12,7 @@ export class Mock { if ((this as any)[method]) { continue; } - (this as any)[method] = mockServiceHelper.mockFunction(this, method); + (this as any)[method] = mockServiceHelper.mockFunction(); } for (const output of (this as any).__mockedOutputs) { if ((this as any)[output]) { diff --git a/lib/mock-component/mock-component.spec.ts b/lib/mock-component/mock-component.spec.ts index 0416baffca..94d932b1c7 100644 --- a/lib/mock-component/mock-component.spec.ts +++ b/lib/mock-component/mock-component.spec.ts @@ -145,7 +145,7 @@ describe('MockComponent', () => { }); it('should allow spying of viewchild component methods', () => { - const spy = spyOn(component.childComponent, 'performAction'); + const spy = component.childComponent.performAction; component.performActionOnChild('test'); expect(spy).toHaveBeenCalledWith('test'); }); diff --git a/lib/mock-directive/mock-directive.spec.ts b/lib/mock-directive/mock-directive.spec.ts index 1990bbb33f..82eb77e649 100644 --- a/lib/mock-directive/mock-directive.spec.ts +++ b/lib/mock-directive/mock-directive.spec.ts @@ -149,7 +149,7 @@ describe('MockDirective', () => { }); it('should allow spying of viewchild directive methods', () => { - const spy = spyOn(component.childDirective, 'performAction'); + const spy = component.childDirective.performAction; component.performActionOnChild('test'); expect(spy).toHaveBeenCalledWith('test'); }); diff --git a/lib/mock-service/mock-service.ts b/lib/mock-service/mock-service.ts index 92cc0a8a48..da11a2bdc7 100644 --- a/lib/mock-service/mock-service.ts +++ b/lib/mock-service/mock-service.ts @@ -1,16 +1,24 @@ export type MockedFunction = () => undefined; -/** - * @internal - */ -export const mockServiceHelper = { - mockFunction: (object?: {}, method?: string): MockedFunction => () => undefined, +let customMockFunction: (() => MockedFunction) | undefined; + +const mockServiceHelperPrototype = { + mockFunction: (): MockedFunction => { + if (customMockFunction) { + return customMockFunction(); + } + return () => undefined; + }, + + registerMockFunction: (mockFunction: typeof customMockFunction) => { + customMockFunction = mockFunction; + }, createMockFromPrototype: (service: any): { [key: string]: MockedFunction } => { const methods = mockServiceHelper.extractMethodsFromPrototype(service); const value: { [key: string]: MockedFunction } = {}; for (const method of methods) { - value[method] = mockServiceHelper.mockFunction(value, method); + value[method] = mockServiceHelper.mockFunction(); } return value; }, @@ -36,6 +44,16 @@ export const mockServiceHelper = { }, }; +// We need a single pointer to the object among all environments. +(window as any || global as any).ngMocksMockServiceHelper = + (window as any || global as any).ngMocksMockServiceHelper || mockServiceHelperPrototype; + +/** + * @internal + */ +export const mockServiceHelper: typeof mockServiceHelperPrototype = + (window as any || global as any).ngMocksMockServiceHelper; + export function MockService(service?: boolean | number | string | null): undefined; export function MockService(service: T): any; export function MockService(service: any): any { diff --git a/mockery/jasmine.ts b/mockery/jasmine.ts new file mode 100644 index 0000000000..c788d81823 --- /dev/null +++ b/mockery/jasmine.ts @@ -0,0 +1,5 @@ +import { mockServiceHelper } from '../lib/mock-service'; + +declare const jasmine: any; + +mockServiceHelper.registerMockFunction(jasmine.createSpy); diff --git a/mockery/jest.ts b/mockery/jest.ts new file mode 100644 index 0000000000..8f92ada0e4 --- /dev/null +++ b/mockery/jest.ts @@ -0,0 +1,5 @@ +import { mockServiceHelper } from '../lib/mock-service'; + +declare const jest: any; + +mockServiceHelper.registerMockFunction(jest.fn); diff --git a/tsconfig.json b/tsconfig.json index cf876657fd..cc34e669cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,5 +23,8 @@ }, "skipLibCheck": true }, - "include": ["index.ts"] + "include": ["index.ts", + "mockery/jasmine.ts", + "mockery/jest.ts" + ] }