Skip to content

Commit

Permalink
feat: support to inject a library-related service mocker
Browse files Browse the repository at this point in the history
closes #87
closes #103
  • Loading branch information
satanTime committed Apr 26, 2020
1 parent 28b452b commit e6be694
Show file tree
Hide file tree
Showing 15 changed files with 368 additions and 33 deletions.
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,9 +439,56 @@ returns attribute or structural directive which belongs to current element.
`MockHelper.findDirective(fixture.debugElement, Directive)` -
returns first found attribute or structural directive which belongs to current element or any child.

`MockHelper.findDirectives(fixture.debugElement, Directive)`
`MockHelper.findDirectives(fixture.debugElement, Directive)` -
returns all found attribute or structural directives which belong to current element and all its child.

`MockHelper.mockService(instance, methodName)` -
returns a mocked function / spy of the method. If the method hasn't been mocked yet - mocks it.

`MockHelper.mockService(instance, propertyName, 'get' | 'set')` -
returns a mocked function / spy of the property. If the property hasn't been mocked yet - mocks it.

```typescript
// The example below uses auto spy.
it('mocks getters, setters and methods in a way that jasmine can mock them w/o an issue', () => {
const mock: GetterSetterMethodHuetod = MockService(GetterSetterMethodHuetod);
expect(mock).toBeDefined();

// Creating a mock on the getter.
MockHelper.mockService<Spy>(mock, 'name', 'get').and.returnValue('mock');
expect(mock.name).toEqual('mock');

// Creating a mock on the setter.
MockHelper.mockService(mock, 'name', 'set');
mock.name = 'mock';
expect(MockHelper.mockService(mock, 'name', 'set')).toHaveBeenCalledWith('mock');

// Creating a mock on the method.
MockHelper.mockService<Spy>(mock, 'nameMethod').and.returnValue('mock');
expect(mock.nameMethod('mock')).toEqual('mock');
expect(MockHelper.mockService(mock, 'nameMethod')).toHaveBeenCalledWith('mock');

// Creating a mock on the method that doesn't exist.
MockHelper.mockService<Spy>(mock, 'fakeMethod').and.returnValue('mock');
expect((mock as any).fakeMethod('mock')).toEqual('mock');
expect(MockHelper.mockService(mock, 'fakeMethod')).toHaveBeenCalledWith('mock');
});
```

## 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/jasmine';
```

In case of jest.

```typescript
import 'ng-mocks/dist/jest';
```

## Other examples of tests

More detailed examples can be found in
Expand Down
19 changes: 19 additions & 0 deletions e2e/spies/fixtures.components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import { TargetService } from './fixtures.providers';

@Component({
selector: 'target',
template: '<ng-content></ng-content>',
})
export class TargetComponent {
protected service: TargetService;

constructor(service: TargetService) {
this.service = service;
this.service.echo('constructor');
}

public echo(): string {
return this.service.echo('TargetComponent');
}
}
7 changes: 7 additions & 0 deletions e2e/spies/fixtures.modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { TargetService } from './fixtures.providers';

@NgModule({
providers: [TargetService],
})
export class TargetModule {}
10 changes: 10 additions & 0 deletions e2e/spies/fixtures.providers.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
73 changes: 73 additions & 0 deletions e2e/spies/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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>('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
}));
});
5 changes: 5 additions & 0 deletions jasmine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { mockServiceHelper } from './lib/mock-service';

declare const jasmine: any;

mockServiceHelper.registerMockFunction(mockName => jasmine.createSpy(mockName));
5 changes: 5 additions & 0 deletions jest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { mockServiceHelper } from './lib/mock-service';

declare const jest: any;

mockServiceHelper.registerMockFunction(() => jest.fn());
1 change: 1 addition & 0 deletions karma.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = (config: any) => {
'node_modules/zone.js/dist/fake-async-test.js',
'karma-test-shim.ts',
'index.ts',
'jasmine.ts',
{ pattern: 'lib/**/*.ts' },
{ pattern: 'e2e/**/*.ts' },
{ pattern: 'examples/**/*.ts' },
Expand Down
2 changes: 1 addition & 1 deletion lib/common/Mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down
2 changes: 1 addition & 1 deletion lib/mock-component/mock-component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
2 changes: 1 addition & 1 deletion lib/mock-directive/mock-directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
5 changes: 5 additions & 0 deletions lib/mock-helper/mock-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { DebugNode, Type } from '@angular/core';

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

interface INestedNodes extends DebugNode {
childNodes?: INestedNodes[];
}
Expand Down Expand Up @@ -70,4 +72,7 @@ export const MockHelper = {
});
return result;
},

mockService: <T = MockedFunction>(instance: any, name: string, style?: 'get' | 'set'): T =>
mockServiceHelper.mock(instance, name, style),
};
76 changes: 74 additions & 2 deletions lib/mock-service/mock-service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MockService } from 'ng-mocks';
import { InjectionToken } from '@angular/core';
import { MockHelper, MockService } from 'ng-mocks';

// tslint:disable:max-classes-per-file
class DeepParentClass {
Expand Down Expand Up @@ -35,6 +36,25 @@ class ChildClass extends ParentClass {
}
}

class GetterSetterMethodHuetod {
public nameValue = 'nameValue';

get name(): string {
return `${this.nameValue}${this.nameValue}`;
}

set name(value: string) {
this.nameValue = value;
}

public nameMethod(value?: string): string {
if (value) {
this.name = value;
}
return this.name;
}
}

// tslint:enable:max-classes-per-file

describe('MockService', () => {
Expand All @@ -55,25 +75,36 @@ describe('MockService', () => {
expect(MockService([new DeepParentClass()])).toEqual([]);
});

it('should convert functions to () => undefined', () => {
it('should convert arrow functions to () => undefined', () => {
const mockedService = MockService(() => 0);
expect(mockedService).toEqual(jasmine.any(Function), 'mockedService');
expect(mockedService()).toBeUndefined();
expect(mockedService.and.identity()).toBe('func:arrow-function');
});

it('should convert normal functions to an empty object because it is a class signature', () => {
const mockedService = MockService(function test() {
return 0;
});
expect(mockedService).toEqual(jasmine.any(Object), 'mockedService');
});

it('should mock own methods of a class without a parent', () => {
const mockedService = MockService(DeepParentClass);
expect(mockedService).toEqual(jasmine.any(Object));

// all properties should be undefined, maybe defined as getters and setters.
expect(mockedService.deepParentMethodName).toBeUndefined('deepParentMethodName');

// all methods should be defined as functions which return undefined.
expect(mockedService.deepParentMethod).toEqual(jasmine.any(Function), 'deepParentMethod');
expect(mockedService.deepParentMethod()).toBeUndefined('deepParentMethod()');
expect(mockedService.deepParentMethod.and.identity()).toBe('DeepParentClass.deepParentMethod');
});

it('should mock own and parent methods of a class', () => {
const mockedService = MockService(ChildClass);
expect(mockedService).toEqual(jasmine.any(ChildClass));

// all properties should be undefined, maybe defined as getters and setters.
expect(mockedService.deepParentMethodName).toBeUndefined('deepParentMethodName');
Expand All @@ -84,16 +115,21 @@ describe('MockService', () => {
// all methods should be defined as functions which return undefined.
expect(mockedService.deepParentMethod).toEqual(jasmine.any(Function), 'deepParentMethod');
expect(mockedService.deepParentMethod()).toBeUndefined('deepParentMethod()');
expect(mockedService.deepParentMethod.and.identity()).toBe('ChildClass.deepParentMethod');
expect(mockedService.parentMethod).toEqual(jasmine.any(Function), 'parentMethod');
expect(mockedService.parentMethod()).toBeUndefined('parentMethod()');
expect(mockedService.parentMethod.and.identity()).toBe('ChildClass.parentMethod');
expect(mockedService.overrideMe).toEqual(jasmine.any(Function), 'overrideMe');
expect(mockedService.overrideMe()).toBeUndefined('overrideMe()');
expect(mockedService.overrideMe.and.identity()).toBe('ChildClass.overrideMe');
expect(mockedService.childMethod).toEqual(jasmine.any(Function), 'childMethod');
expect(mockedService.childMethod()).toBeUndefined('childMethod()');
expect(mockedService.childMethod.and.identity()).toBe('ChildClass.childMethod');
});

it('should mock an instance of a class as an object', () => {
const mockedService = MockService(new ChildClass());
expect(mockedService).toEqual(jasmine.any(ChildClass));

// all properties should be undefined, maybe defined as getters and setters.
expect(mockedService.deepParentMethodName).toBeUndefined('deepParentMethodName');
Expand All @@ -104,12 +140,16 @@ describe('MockService', () => {
// all methods should be defined as functions which return undefined.
expect(mockedService.deepParentMethod).toEqual(jasmine.any(Function), 'deepParentMethod');
expect(mockedService.deepParentMethod()).toBeUndefined('deepParentMethod()');
expect(mockedService.deepParentMethod.and.identity()).toBe('ChildClass.deepParentMethod');
expect(mockedService.parentMethod).toEqual(jasmine.any(Function), 'parentMethod');
expect(mockedService.parentMethod()).toBeUndefined('parentMethod()');
expect(mockedService.parentMethod.and.identity()).toBe('ChildClass.parentMethod');
expect(mockedService.overrideMe).toEqual(jasmine.any(Function), 'overrideMe');
expect(mockedService.overrideMe()).toBeUndefined('overrideMe()');
expect(mockedService.overrideMe.and.identity()).toBe('ChildClass.overrideMe');
expect(mockedService.childMethod).toEqual(jasmine.any(Function), 'childMethod');
expect(mockedService.childMethod()).toBeUndefined('childMethod()');
expect(mockedService.childMethod.and.identity()).toBe('ChildClass.childMethod');
});

it('should mock own and nested properties of an object', () => {
Expand Down Expand Up @@ -144,7 +184,39 @@ describe('MockService', () => {
});

expect(mockedService.child1.child11.func1()).toBeUndefined('func1()');
expect(mockedService.child1.child11.func1.and.identity()).toBe('func:instance.child1.child11.func1');
expect(mockedService.func2()).toBeUndefined('func2()');
expect(mockedService.func2.and.identity()).toBe('func:instance.func2');
expect(mockedService.func3()).toBeUndefined('func3()');
expect(mockedService.func3.and.identity()).toBe('func:instance.func3');
});

it('mocks getters, setters and methods in a way that jasmine can mock them w/o an issue', () => {
const mock: GetterSetterMethodHuetod = MockService(GetterSetterMethodHuetod);
expect(mock).toBeDefined();

// Creating a mock on the getter.
MockHelper.mockService<jasmine.Spy>(mock, 'name', 'get').and.returnValue('mock');
expect(mock.name).toEqual('mock');

// Creating a mock on the setter.
MockHelper.mockService(mock, 'name', 'set');
mock.name = 'mock';
expect(MockHelper.mockService(mock, 'name', 'set')).toHaveBeenCalledWith('mock');

// Creating a mock on the method.
MockHelper.mockService<jasmine.Spy>(mock, 'nameMethod').and.returnValue('mock');
expect(mock.nameMethod('mock')).toEqual('mock');
expect(MockHelper.mockService(mock, 'nameMethod')).toHaveBeenCalledWith('mock');

// Creating a mock on the method that doesn't exist.
MockHelper.mockService<jasmine.Spy>(mock, 'fakeMethod').and.returnValue('mock');
expect((mock as any).fakeMethod('mock')).toEqual('mock');
expect(MockHelper.mockService(mock, 'fakeMethod')).toHaveBeenCalledWith('mock');
});

it('mocks injection tokens as undefined', () => {
const token1 = MockService(new InjectionToken('hello'));
expect(token1).toBeUndefined();
});
});
Loading

0 comments on commit e6be694

Please sign in to comment.