Skip to content

Commit

Permalink
fix: mocking token more intelligently
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Oct 25, 2020
1 parent c1c6175 commit 0f7cc0c
Show file tree
Hide file tree
Showing 11 changed files with 504 additions and 48 deletions.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -751,8 +751,8 @@ describe('MockPipe', () => {

`ngMocks` provides a `MockService` function that tries its best
to facilitate creation of mocked copies of services.
It tends to avoid hustle of providing customized mocks for huge services.
Simply pass a class into it and its result wil be a mocked instance that respects the class,
It tends to avoid hustle of providing customized mocked copies for huge services.
Simply pass a class into it and its result will be a mocked instance that respects the class,
but all methods and properties are customizable dummies.

- `MockService(MyService)` - returns a mocked instance of `MyService` class.
Expand Down Expand Up @@ -898,11 +898,15 @@ It extends features of `MockModule`.</small>
**A mocked module** respects its original module as
a type of `MockedModule<T>` and provides:

- mocks all components, directives, pipes
- mocks all services as their dummy instances
- mocks all components, directives, pipes and providers
- mocks all imports and exports
- mocks tokens with `useValue` definition as primitives such as `0`, `false`, `''`, `null` and `undefined`
- ignores all other token to avoid their influence
- mocks all services as their dummy instances
- mocks abstract methods of services with a `useClass` definition
- mocks tokens with a `useClass` definition based on its value
- forwards tokens with a `useExisting` definition to its value
- mocks tokens with a `useFactory` definition as an empty object
- mocks tokens with a `useValue` definition as a primitive such as `0`, `false`, `''`, `null` and `undefined`
- mocks tokens with a `useValue` definition as an object via [`MockService`](#how-to-mock-a-service)

Let's imagine an Angular application where `TargetComponent` depends on a module of `DependencyModule`
and we would like to mock in a test.
Expand Down
7 changes: 6 additions & 1 deletion examples/TestRoutingGuard/test.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Location } from '@angular/common';
import { Component, Injectable, NgModule } from '@angular/core';
import { Component, Injectable, NgModule, VERSION } from '@angular/core';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { CanActivate, Router, RouterModule, RouterOutlet } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
Expand Down Expand Up @@ -84,6 +84,11 @@ describe('TestRoutingGuard', () => {

// It is important to run routing tests in fakeAsync.
it('redirects to login', fakeAsync(() => {
if (parseInt(VERSION.major, 10) <= 6) {
pending('Need Angular > 6');
return;
}

const fixture = MockRender(RouterOutlet);
const router: Router = TestBed.get(Router);
const location: Location = TestBed.get(Location);
Expand Down
8 changes: 3 additions & 5 deletions lib/mock-builder/mock-builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { InjectionToken } from '@angular/core';
import { MetadataOverride, TestBed, TestModuleMetadata } from '@angular/core/testing';

import { AnyType, flatten, isNgDef, isNgInjectionToken, mapEntries, NG_MOCKS, NG_MOCKS_OVERRIDES } from '../common/lib';
import { AnyType, flatten, isNgDef, mapEntries, NG_MOCKS, NG_MOCKS_OVERRIDES } from '../common/lib';
import { ngMocksUniverse } from '../common/ng-mocks-universe';
import { ngMocks } from '../mock-helper/mock-helper';

Expand All @@ -10,7 +10,7 @@ import { MockBuilderPromise } from './mock-builder-promise';

export function MockBuilder(
keepDeclaration?: AnyType<any> | InjectionToken<any>,
itsModuleToMock?: AnyType<any> | InjectionToken<any>
itsModuleToMock?: AnyType<any>
): MockBuilderPromise {
if (!(TestBed as any).ngMocks) {
const configureTestingModule = TestBed.configureTestingModule;
Expand Down Expand Up @@ -99,9 +99,7 @@ export function MockBuilder(
export: true,
});
}
if (itsModuleToMock && isNgInjectionToken(itsModuleToMock)) {
instance.mock(itsModuleToMock);
} else if (itsModuleToMock) {
if (itsModuleToMock) {
instance.mock(itsModuleToMock, {
exportAll: true,
});
Expand Down
125 changes: 93 additions & 32 deletions lib/mock-module/mock-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ import { MockService, mockServiceHelper } from '../mock-service';
export type MockedModule<T> = T & Mock & {};

const neverMockProvidedFunction = ['DomRendererFactory2', 'DomSharedStylesHost', 'EventManager', 'RendererFactory2'];
const neverMockToken = [
// RouterModule
'InjectionToken Application Initializer',
// BrowserModule
'InjectionToken EventManagerPlugins',
'InjectionToken HammerGestureConfig',
];

/**
* Can be changed any time.
Expand All @@ -33,51 +40,105 @@ const neverMockProvidedFunction = ['DomRendererFactory2', 'DomSharedStylesHost',
*/
export function MockProvider(provider: any): Provider | undefined {
const provide = typeof provider === 'object' && provider.provide ? provider.provide : provider;
if (ngMocksUniverse.flags.has('cacheProvider') && ngMocksUniverse.cacheProviders.has(provide)) {

if (typeof provide === 'function' && neverMockProvidedFunction.indexOf(provide.name) !== -1) {
return provider;
}
if (isNgInjectionToken(provide) && neverMockToken.indexOf(provide.toString()) !== -1) {
return undefined;
}

// Only pure provides should be cached to avoid their influence on
// another different declarations.
if (
provide === provider &&
ngMocksUniverse.flags.has('cacheProvider') &&
ngMocksUniverse.cacheProviders.has(provide)
) {
return ngMocksUniverse.cacheProviders.get(provide);
}

let mockedProvider: Provider | undefined;
if (typeof provide === 'function' && !mockedProvider) {
mockedProvider = mockServiceHelper.useFactory(ngMocksUniverse.cacheMocks.get(provide) || provide, () => {
const instance = MockService(provide);
// Magic below adds missed properties to the instance to
// fulfill missed abstract methods.
if (provide !== provider && Object.keys(provider).indexOf('useClass') !== -1) {
const existing = Object.getOwnPropertyNames(instance);
const child = MockService(provider.useClass);
for (const name of Object.getOwnPropertyNames(child)) {
if (existing.indexOf(name) !== -1) {
continue;
}
const def = Object.getOwnPropertyDescriptor(child, name);
/* istanbul ignore else */
if (def) {
Object.defineProperty(instance, name, def);
}
}
}
return instance;
});
}

if (provide === provider && mockedProvider && ngMocksUniverse.flags.has('cacheProvider')) {
ngMocksUniverse.cacheProviders.set(provide, mockedProvider);
}
if (mockedProvider) {
return mockedProvider;
}

// Not sure if this case is possible, all classes should be already
// mocked by the code above, below we should have only tokens and
// string literals with a proper definition.
if (provide === provider) {
return undefined;
}

// Tokens are special subject, we can skip adding them because in a mocked module they are useless.
// The main problem is that providing undefined to HTTP_INTERCEPTORS and others breaks their code.
// If a testing module / component requires omitted tokens then they should be provided manually
// during creation of TestBed module.
if (isNgInjectionToken(provide) && provider.multi) {
if (provider.multi) {
return undefined;
}

// if a token has a primitive type, we can return its initial state.
if (isNgInjectionToken(provide) && Object.keys(provider).indexOf('useValue') !== -1) {
return provider.useValue && typeof provider.useValue === 'object'
? mockServiceHelper.useFactory(ngMocksUniverse.cacheMocks.get(provide) || provide, () =>
MockService(provider.useValue)
)
: {
provide,
useValue:
typeof provider.useValue === 'boolean'
? false
: typeof provider.useValue === 'number'
? 0
: typeof provider.useValue === 'string'
? ''
: provider.useValue === null
? null
: undefined,
};
if (!mockedProvider && Object.keys(provider).indexOf('useValue') !== -1) {
mockedProvider =
provider.useValue && typeof provider.useValue === 'object'
? mockServiceHelper.useFactory(ngMocksUniverse.cacheMocks.get(provide) || provide, () =>
MockService(provider.useValue)
)
: {
provide,
useValue:
typeof provider.useValue === 'boolean'
? false
: typeof provider.useValue === 'number'
? 0
: typeof provider.useValue === 'string'
? ''
: provider.useValue === null
? null
: undefined,
};
}
if (isNgInjectionToken(provide)) {
return undefined;
if (!mockedProvider && Object.keys(provider).indexOf('useExisting') !== -1) {
mockedProvider = provider;
}

if (typeof provide === 'function' && neverMockProvidedFunction.indexOf(provide.name) !== -1) {
return provider;
if (!mockedProvider && Object.keys(provider).indexOf('useClass') !== -1) {
mockedProvider =
ngMocksUniverse.builder.has(provider.useClass) &&
ngMocksUniverse.builder.get(provider.useClass) === provider.useClass
? provider
: mockServiceHelper.useFactory(ngMocksUniverse.cacheMocks.get(provide) || provide, () =>
MockService(provider.useClass)
);
}

const mockedProvider = mockServiceHelper.useFactory(ngMocksUniverse.cacheMocks.get(provide) || provide, () =>
MockService(provide)
);
/* istanbul ignore else */
if (ngMocksUniverse.flags.has('cacheProvider')) {
ngMocksUniverse.cacheProviders.set(provide, mockedProvider);
if (!mockedProvider && Object.keys(provider).indexOf('useFactory') !== -1) {
mockedProvider = mockServiceHelper.useFactory(ngMocksUniverse.cacheMocks.get(provide) || provide, () => ({}));
}

return mockedProvider;
Expand Down
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ng-mocks",
"version": "10.4.0",
"description": "Functions for creating angular mocks",
"description": "A library mocking angular components, directives, services and more",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
Expand Down Expand Up @@ -42,8 +42,8 @@
"clear:a11": "rm -Rf e2e/a11/node_modules/ng-mocks && rm -Rf e2e/a11/test",
"s:test:a": "npm run s:test:a5 & npm run s:test:a6 & npm run s:test:a7 & npm run s:test:a8 & npm run s:test:a9 & npm run s:test:a10 & npm run s:test:a11 & wait",
"s:test:a5": "npm run s:test:a5es5 && npm run s:test:a5es2015",
"s:test:a5es5": "P=e2e/a5es5/src/test && rm -Rf $P && mkdir -p $P && cp -R tests $P && cp -R examples $P",
"s:test:a5es2015": "P=e2e/a5es2015/src/test && rm -Rf $P && mkdir -p $P && cp -R tests $P && cp -R examples $P",
"s:test:a5es5": "P=e2e/a5es5/src/test && rm -Rf $P && mkdir -p $P && cp -R tests $P && cp -R examples $P && rm $P/examples/TestRoutingGuard/test.spec.ts && rm $P/examples/TestRoutingResolver/test.spec.ts",
"s:test:a5es2015": "P=e2e/a5es2015/src/test && rm -Rf $P && mkdir -p $P && cp -R tests $P && cp -R examples $P && rm $P/examples/TestRoutingGuard/test.spec.ts && rm $P/examples/TestRoutingResolver/test.spec.ts",
"s:test:a6": "P=e2e/a6/src/test && rm -Rf $P && mkdir -p $P && cp -R tests $P && cp -R examples $P",
"s:test:a7": "P=e2e/a7/src/test && rm -Rf $P && mkdir -p $P && cp -R tests $P && cp -R examples $P",
"s:test:a8": "P=e2e/a8/src/test && rm -Rf $P && mkdir -p $P && cp -R tests $P && cp -R examples $P",
Expand Down Expand Up @@ -78,10 +78,14 @@
},
"keywords": [
"Angular",
"Mock",
"Module",
"Component",
"Directive",
"Mock",
"Pipe",
"Provider",
"Service",
"Token",
"TestBed",
"Testing"
],
Expand Down
37 changes: 37 additions & 0 deletions tests/abstract-methods-provider/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Injectable, NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder } from 'ng-mocks';

@Injectable()
abstract class LoggerInterface {
abstract log(message: string): void;
}

@Injectable()
class Logger implements LoggerInterface {
public lastMessage = '';

log(message: string): void {
this.lastMessage = message;
}
}

@NgModule({
providers: [
{
provide: LoggerInterface,
useClass: Logger,
},
],
})
class TargetModule {}

describe('abstract-methods-provider', () => {
beforeEach(() => MockBuilder().mock(TargetModule));

it('provides a mocked copy with an implemented abstract method', () => {
const actual: LoggerInterface = TestBed.get(LoggerInterface);

expect(actual.log).toBeDefined();
});
});
Loading

0 comments on commit 0f7cc0c

Please sign in to comment.