Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support to inject a library-related service mocker #100

Merged
merged 1 commit into from
May 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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