Skip to content

Commit

Permalink
fix: keeping root providers for kept modules
Browse files Browse the repository at this point in the history
closes #222
  • Loading branch information
satanTime committed Nov 3, 2020
1 parent 468ee09 commit dc078af
Show file tree
Hide file tree
Showing 14 changed files with 485 additions and 44 deletions.
77 changes: 58 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1380,6 +1380,9 @@ and has a rich toolkit that supports:
* [`exportAll` flag](#mockbuilder-exportall-flag)
* [`dependency` flag](#mockbuilder-dependency-flag)
* [`render` flag](#mockbuilder-render-flag)
* [`NG_MOCKS_GUARDS` token](#ng_mocks_guards-token)
* [`NG_MOCKS_INTERCEPTORS` token](#ng_mocks_interceptors-token)
* [`NG_MOCKS_ROOT_PROVIDERS` token](#ng_mocks_root_providers-token)
* [Good to know](#mockbuilder-good-to-know)

<details><summary>Click to see <strong>a code sample demonstrating ease of mocking in Angular tests</strong></summary>
Expand Down Expand Up @@ -1542,25 +1545,6 @@ beforeEach(() =>
);
```

If we want to test guards we need to `.keep` them, but what should we do with other guards we do not want to care about at all?
The answer is to exclude `NG_MOCKS_GUARDS` token, it will removal all the guards from their routes except the explicitly configured ones.

```typescript
beforeEach(() =>
MockBuilder(MyGuard, MyModule).exclude(NG_MOCKS_GUARDS)
);
```

The same thing if we want to test interceptors.
If we exclude `NG_MOCKS_INTERCEPTORS` token, then all interceptors with `useValue` or `useFactory` will be excluded
together with other interceptors except the explicitly configured ones.

```typescript
beforeEach(() =>
MockBuilder(MyInterceptor, MyModule).exclude(NG_MOCKS_INTERCEPTORS)
);
```

#### MockBuilder.replace

If we want to replace something with something, we should use `.replace`.
Expand Down Expand Up @@ -1722,6 +1706,61 @@ beforeEach(() =>
);
```

#### `NG_MOCKS_GUARDS` token

If we want to test guards we need to `.keep` them, but what should we do with other guards we do not want to care about at all?
The answer is to exclude `NG_MOCKS_GUARDS` token, it will **remove all the guards from routes** except the explicitly configured ones.

```typescript
beforeEach(() =>
MockBuilder(MyGuard, MyModule).exclude(NG_MOCKS_GUARDS)
);
```

#### `NG_MOCKS_INTERCEPTORS` token

Usually, when we want to test an interceptor, we want to avoid influences of other interceptors.
To **remove all interceptors in an angular test** we need to exclude `NG_MOCKS_INTERCEPTORS` token,
then all interceptors will be excluded except the explicitly configured ones.

```typescript
beforeEach(() =>
MockBuilder(MyInterceptor, MyModule).exclude(NG_MOCKS_INTERCEPTORS)
);
```

#### `NG_MOCKS_ROOT_PROVIDERS` token

There are root services and tokens apart from provided ones in Angular applications.
It might happen that in a test we want these providers to be mocked, or kept.

If we want to mock all root providers in an angular test we need to mock `NG_MOCKS_ROOT_PROVIDERS` token.

```typescript
beforeEach(() =>
MockBuilder(
MyComponentWithRootServices,
MyModuleWithRootTokens
).mock(NG_MOCKS_ROOT_PROVIDERS)
);
```

In contrast to that, we might want to keep all root providers for mocked declarations.
For that, we need to keep `NG_MOCKS_ROOT_PROVIDERS` token.

```typescript
beforeEach(() =>
MockBuilder(
MyComponentWithRootServices,
MyModuleWithRootTokens
).keep(NG_MOCKS_ROOT_PROVIDERS)
);
```

If we do not pass `NG_MOCKS_ROOT_PROVIDERS` anywhere,
then only root providers for kept modules will stay as they are.
All other root providers will be mocked, even for kept declarations of mocked modules.

#### MockBuilder good to know

Anytime we can change our decision. The last action on the same object wins. SomeModule will be mocked.
Expand Down
10 changes: 4 additions & 6 deletions lib/common/core.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ export default {
neverMockModule: [ApplicationModule, CommonModule],
neverMockProvidedFunction: [
'DomRendererFactory2',
'DomSharedStylesHost',
'EventManager',
'Injector',
'Injector', // ivy only
'RendererFactory2',
],
neverMockToken: [
'InjectionToken Set Injector scope.',
'InjectionToken Application Initializer',
'InjectionToken EventManagerPlugins',
'InjectionToken HammerGestureConfig',
'InjectionToken Set Injector scope.', // INJECTOR_SCOPE // ivy only
'InjectionToken EventManagerPlugins', // EVENT_MANAGER_PLUGINS
'InjectionToken HammerGestureConfig', // HAMMER_GESTURE_CONFIG
],
};
1 change: 1 addition & 0 deletions lib/common/core.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const NG_MOCKS_OVERRIDES = new InjectionToken<Map<Type<any> | AbstractTyp
);
export const NG_MOCKS_GUARDS = new InjectionToken<void>('NG_MOCKS_GUARDS');
export const NG_MOCKS_INTERCEPTORS = new InjectionToken<void>('NG_MOCKS_INTERCEPTORS');
export const NG_MOCKS_ROOT_PROVIDERS = new InjectionToken<void>('NG_MOCKS_ROOT_PROVIDERS');

/**
* Use NG_MOCKS_GUARDS instead.
Expand Down
10 changes: 8 additions & 2 deletions lib/mock-builder/mock-builder-promise.skip-dep.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DOCUMENT } from '@angular/common';
import { EVENT_MANAGER_PLUGINS } from '@angular/platform-browser';
import { isNgInjectionToken } from 'ng-mocks';

import ngConfig from '../common/core.config';
import { ngMocksUniverse } from '../common/ng-mocks-universe';

// Checks if we should avoid mocking of the provider.
Expand All @@ -11,10 +12,15 @@ export default (provide: any): boolean => {
if (ngMocksUniverse.touches.has(provide)) {
return true;
}

if (provide === DOCUMENT) {
return true;
}
if (provide === EVENT_MANAGER_PLUGINS) {

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

Expand Down
50 changes: 33 additions & 17 deletions lib/mock-builder/mock-builder-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MetadataOverride, TestBed } from '@angular/core/testing';

import { extractDependency, flatten, mapEntries, mapValues } from '../common/core.helpers';
import { directiveResolver, jitReflector, ngModuleResolver } from '../common/core.reflect';
import { NG_MOCKS, NG_MOCKS_OVERRIDES, NG_MOCKS_TOUCHES } from '../common/core.tokens';
import { NG_MOCKS, NG_MOCKS_OVERRIDES, NG_MOCKS_ROOT_PROVIDERS, NG_MOCKS_TOUCHES } from '../common/core.tokens';
import { AnyType, Type } from '../common/core.types';
import { isNgDef } from '../common/func.is-ng-def';
import { isNgInjectionToken } from '../common/func.is-ng-injection-token';
Expand Down Expand Up @@ -63,6 +63,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
ngMocksUniverse.touches = new Set();
ngMocksUniverse.config.set('multi', new Set()); // collecting multi flags of providers.
ngMocksUniverse.config.set('deps', new Set()); // collecting all deps of providers.
ngMocksUniverse.config.set('depsSkip', new Set()); // collecting all declarations of kept modules.

for (const def of mapValues(this.keepDef)) {
ngMocksUniverse.builder.set(def, def);
Expand Down Expand Up @@ -213,25 +214,40 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
}
}

// Adding missed providers.
// Mocking root providers.
const parameters = new Set<any>();
if (ngMocksUniverse.touches.size || ngMocksUniverse.config.get('deps').size) {
const touchedDefs: any[] = mapValues(ngMocksUniverse.touches);
touchedDefs.push(...mapValues(ngMocksUniverse.config.get('deps')));
for (const def of touchedDefs) {
if (!skipDep(def)) {
parameters.add(def);
}

for (const decorators of jitReflector.parameters(def)) {
const provide: any = extractDep(decorators);
if (skipDep(provide)) {
continue;
if (!this.keepDef.has(NG_MOCKS_ROOT_PROVIDERS)) {
// We need buckets here to process first all depsSkip, then deps and only after that all other defs.
const buckets: any[] = [];
buckets.push(mapValues(ngMocksUniverse.config.get('depsSkip')));
buckets.push(mapValues(ngMocksUniverse.config.get('deps')));
buckets.push(mapValues(ngMocksUniverse.touches));
// Also we need to track what has been touched to check params recursively, but avoiding duplicates.
const touched: any[] = [].concat(...buckets);
for (const bucket of buckets) {
for (const def of bucket) {
if (!skipDep(def)) {
if (this.mockDef.has(NG_MOCKS_ROOT_PROVIDERS) || !ngMocksUniverse.config.get('depsSkip').has(def)) {
parameters.add(def);
}
}
if (typeof provide === 'function' && touchedDefs.indexOf(provide) === -1) {
touchedDefs.push(provide);

for (const decorators of jitReflector.parameters(def)) {
const provide: any = extractDep(decorators);
if (skipDep(provide)) {
continue;
}
if (typeof provide === 'function' && touched.indexOf(provide) === -1) {
touched.push(provide);
bucket.push(provide);
}

if (this.mockDef.has(NG_MOCKS_ROOT_PROVIDERS) || !ngMocksUniverse.config.get('depsSkip').has(def)) {
parameters.add(provide);
} else {
ngMocksUniverse.config.get('depsSkip').add(provide);
}
}
parameters.add(provide);
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions lib/mock-module/mock-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export function MockModule(module: any): any {
if (!mockModule) {
mockModule = ngModule;
}
if (ngMocksUniverse.flags.has('skipMock')) {
ngMocksUniverse.config.get('depsSkip')?.add(mockModule);
}

if (ngModuleProviders) {
const [changed, ngModuleDef] = MockNgDef({ providers: ngModuleProviders });
Expand Down Expand Up @@ -191,6 +194,10 @@ export function MockNgDef(ngModuleDef: NgModule, ngModule?: Type<any>): [boolean
mockedDef = MockPipe(def);
}

if (ngMocksUniverse.flags.has('skipMock')) {
ngMocksUniverse.config.get('depsSkip')?.add(mockedDef);
}

resolutions.set(def, mockedDef);
changed = changed || mockedDef !== def;
return mockedDef;
Expand Down
1 change: 1 addition & 0 deletions lib/mock-service/helper.resolve-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default (def: any, resolutions: Map<any, any>, changed?: (flag: boolean)
}

if (!mockedDef && ngMocksUniverse.flags.has('skipMock')) {
ngMocksUniverse.config.get('depsSkip')?.add(provider);
mockedDef = def;
}
if (!mockedDef) {
Expand Down
108 changes: 108 additions & 0 deletions tests/NG_MOCKS_ROOT_PROVIDERS/test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Component, Injectable as InjectableSource, NgModule, VERSION } from '@angular/core';
import { MockBuilder, MockRender, NG_MOCKS_ROOT_PROVIDERS } from 'ng-mocks';

// Because of A5 we need to cast Injectable to any type.
// But because of A10+ we need to do it via a middle function.
function Injectable(...args: any[]): any {
return InjectableSource(...args);
}

@Injectable({
providedIn: 'root',
})
class Target1Service {
public readonly name = 'target-1';
}

@Component({
selector: 'target-1',
template: `{{ service.name }}`,
})
class Target1Component {
public readonly service: Target1Service;

constructor(service: Target1Service) {
this.service = service;
}
}

@NgModule({
declarations: [Target1Component],
exports: [Target1Component],
})
class Target1Module {}

@Injectable({
providedIn: 'root',
})
class Target2Service {
public readonly name = 'target-2';
}

@Component({
selector: 'target-2',
template: `{{ service.name }}`,
})
class Target2Component {
public readonly service: Target2Service;

constructor(service: Target2Service) {
this.service = service;
}
}

@NgModule({
declarations: [Target2Component],
exports: [Target2Component],
})
class Target2Module {}

@NgModule({
exports: [Target1Module, Target2Module],
imports: [Target1Module, Target2Module],
})
class CombinedModule {}

describe('NG_MOCKS_ROOT_PROVIDERS', () => {
beforeEach(() => {
if (parseInt(VERSION.major, 10) <= 5) {
pending('Need Angular > 5');
}
});

describe('default for a kept module', () => {
beforeEach(() => MockBuilder(Target1Component, CombinedModule).keep(Target1Module));

it('keeps its global service', () => {
const fixture = MockRender(Target1Component);
expect(fixture.nativeElement.innerHTML).toEqual('<target-1>target-1</target-1>');
});
});

describe('mock the token', () => {
beforeEach(() => MockBuilder(Target1Component, CombinedModule).keep(Target1Module).mock(NG_MOCKS_ROOT_PROVIDERS));

it('mocks global service for a kept module', () => {
const fixture = MockRender(Target1Component);
expect(fixture.nativeElement.innerHTML).toEqual('<target-1></target-1>');
});
});

describe('default for a mocked module', () => {
beforeEach(() => MockBuilder(Target1Component, CombinedModule));

it('mocks its global service', () => {
const fixture = MockRender(Target1Component);
expect(fixture.nativeElement.innerHTML).toEqual('<target-1></target-1>');
});
});

describe('keep the token', () => {
beforeEach(() => MockBuilder(Target1Component, CombinedModule).keep(NG_MOCKS_ROOT_PROVIDERS));

it('keeps global service for a mocked module', () => {
const fixture = MockRender(Target1Component);
expect(fixture.nativeElement.innerHTML).toEqual('<target-1>target-1</target-1>');
});
});
});
Loading

0 comments on commit dc078af

Please sign in to comment.