Skip to content

Commit

Permalink
Merge pull request #224 from satanTime/issues/222
Browse files Browse the repository at this point in the history
Issues/222
  • Loading branch information
satanTime authored Nov 4, 2020
2 parents 0bdd770 + e5486e6 commit 0319543
Show file tree
Hide file tree
Showing 23 changed files with 755 additions and 106 deletions.
85 changes: 63 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1381,6 +1381,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 @@ -1543,23 +1546,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_GUARDS` token, it will removal all the guards from their routes except the explicitly configured ones.

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

The same thing if we want to test interceptors.
If we exclude `NG_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_INTERCEPTORS)
);
```

#### MockBuilder.replace

If we want to replace something with something, we should use `.replace`.
Expand Down Expand Up @@ -1721,6 +1707,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 Expand Up @@ -2978,13 +3019,13 @@ If you did not read ["How to test a route"](#how-to-test-a-route), please do it

To test a guard means that we need to mock everything except the guard and `RouterModule`.
But, what if we have several guards? If we mocked them they would block routes due to falsy returns of their mocked methods.
**To skip guards in angular tests `ngMocks` provides `NG_GUARDS` token**, we should pass it into `.exclude`, then all other guards will be
**To skip guards in angular tests `ngMocks` provides `NG_MOCKS_GUARDS` token**, we should pass it into `.exclude`, then all other guards will be
excluded from `TestBed` and we can be sure, that we are **testing only the guard we want**.

```typescript
beforeEach(() =>
MockBuilder(LoginGuard, TargetModule)
.exclude(NG_GUARDS)
.exclude(NG_MOCKS_GUARDS)
.keep(RouterModule)
.keep(RouterTestingModule.withRoutes([]))
);
Expand Down Expand Up @@ -3027,7 +3068,7 @@ Optionally, we can disable guards to avoid influence of their mocked methods ret
```typescript
beforeEach(() =>
MockBuilder(DataResolver, TargetModule)
.exclude(NG_GUARDS)
.exclude(NG_MOCKS_GUARDS)
.keep(RouterModule)
.keep(RouterTestingModule.withRoutes([]))
);
Expand Down Expand Up @@ -3147,7 +3188,7 @@ The problem of `useValue` and `useFactory` is that it is quite hard to distingui
in `TestBed`.

We need to keep `HTTP_INTERCEPTORS` token, because the interceptor is defined by it.
But this cause that all other interceptors will be kept too, therefore, we need to get rid of them via excluding `NG_INTERCEPTORS` token.
But this cause that all other interceptors will be kept too, therefore, we need to get rid of them via excluding `NG_MOCKS_INTERCEPTORS` token.
The issue here is that if there are more interceptors, then their mocked copies will fail
with "You provided 'undefined' where a stream was expected." error.
And the last important step is to replace `HttpClientModule` with `HttpClientTestingModule`,
Expand All @@ -3156,7 +3197,7 @@ so we can use `HttpTestingController` for faking requests.
```typescript
beforeEach(() =>
MockBuilder(TargetInterceptor, TargetModule)
.exclude(NG_INTERCEPTORS)
.exclude(NG_MOCKS_INTERCEPTORS)
.keep(HTTP_INTERCEPTORS)
.replace(HttpClientModule, HttpClientTestingModule)
);
Expand Down
4 changes: 2 additions & 2 deletions examples/TestHttpInterceptor/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injectable, NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder, NG_INTERCEPTORS } from 'ng-mocks';
import { MockBuilder, NG_MOCKS_INTERCEPTORS } from 'ng-mocks';
import { Observable } from 'rxjs';

// An interceptor we want to test.
Expand Down Expand Up @@ -73,7 +73,7 @@ describe('TestHttpInterceptor', () => {
// with HttpClientTestingModule.
beforeEach(() =>
MockBuilder(TargetInterceptor, TargetModule)
.exclude(NG_INTERCEPTORS)
.exclude(NG_MOCKS_INTERCEPTORS)
.keep(HTTP_INTERCEPTORS)
.replace(HttpClientModule, HttpClientTestingModule)
);
Expand Down
9 changes: 6 additions & 3 deletions examples/TestRoutingGuard/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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';
import { MockBuilder, MockRender, NG_GUARDS, ngMocks } from 'ng-mocks';
import { MockBuilder, MockRender, NG_MOCKS_GUARDS, ngMocks } from 'ng-mocks';
import { from, Observable } from 'rxjs';
import { mapTo } from 'rxjs/operators';

Expand Down Expand Up @@ -99,9 +99,12 @@ describe('TestRoutingGuard', () => {
// RouterModule to have its routes, and to add
// RouterTestingModule.withRoutes([]), yes yes, with empty routes
// to have tools for testing. And the last thing is to exclude
// `NG_GUARDS` to remove all other guards.
// `NG_MOCKS_GUARDS` to remove all other guards.
beforeEach(() =>
MockBuilder(LoginGuard, TargetModule).exclude(NG_GUARDS).keep(RouterModule).keep(RouterTestingModule.withRoutes([]))
MockBuilder(LoginGuard, TargetModule)
.exclude(NG_MOCKS_GUARDS)
.keep(RouterModule)
.keep(RouterTestingModule.withRoutes([]))
);

// It is important to run routing tests in fakeAsync.
Expand Down
17 changes: 17 additions & 0 deletions lib/common/core.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { ApplicationModule } from '@angular/core';

export default {
neverMockModule: [ApplicationModule, CommonModule],
neverMockProvidedFunction: [
'DomRendererFactory2',
'EventManager',
'Injector', // ivy only
'RendererFactory2',
],
neverMockToken: [
'InjectionToken Set Injector scope.', // INJECTOR_SCOPE // ivy only
'InjectionToken EventManagerPlugins', // EVENT_MANAGER_PLUGINS
'InjectionToken HammerGestureConfig', // HAMMER_GESTURE_CONFIG
],
};
19 changes: 17 additions & 2 deletions lib/common/core.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,20 @@ export const NG_MOCKS_TOUCHES = new InjectionToken<Set<any>>('NG_MOCKS_TOUCHES')
export const NG_MOCKS_OVERRIDES = new InjectionToken<Map<Type<any> | AbstractType<any>, MetadataOverride<any>>>(
'NG_MOCKS_OVERRIDES'
);
export const NG_GUARDS = new InjectionToken<void>('NG_MOCKS_GUARDS');
export const NG_INTERCEPTORS = new InjectionToken<void>('NG_MOCKS_INTERCEPTORS');
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.
* Will be removed in v11.
* @deprecated
*/
export const NG_GUARDS = NG_MOCKS_GUARDS;

/**
* Use NG_MOCKS_INTERCEPTORS instead.
* Will be removed in v11.
* @deprecated
*/
export const NG_INTERCEPTORS = NG_MOCKS_INTERCEPTORS;
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
72 changes: 47 additions & 25 deletions lib/mock-builder/mock-builder-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { InjectionToken, NgModule, PipeTransform, Provider } from '@angular/core
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 { directiveResolver, jitReflector } from '../common/core.reflect';
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,21 +63,27 @@ 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.
ngMocksUniverse.config.set('resolution', new Map()); // flags to understand how to mock nested declarations.

for (const def of mapValues(this.keepDef)) {
ngMocksUniverse.builder.set(def, def);
ngMocksUniverse.config.get('resolution').set(def, 'keep');
}

for (const source of mapValues(this.replaceDef)) {
ngMocksUniverse.builder.set(source, this.defValue.get(source));
for (const def of mapValues(this.replaceDef)) {
ngMocksUniverse.builder.set(def, this.defValue.get(def));
ngMocksUniverse.config.get('resolution').set(def, 'replace');
}

for (const def of [...mapValues(this.excludeDef)]) {
ngMocksUniverse.builder.set(def, null);
ngMocksUniverse.config.get('resolution').set(def, 'exclude');
}

// mocking requested things.
for (const def of mapValues(this.mockDef)) {
ngMocksUniverse.config.get('resolution').set(def, 'mock');
if (isNgDef(def)) {
continue;
}
Expand Down Expand Up @@ -111,7 +117,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {

// Now we need to run through requested modules.
const defProviders = new Map();
for (const def of [...mapValues(this.mockDef), ...mapValues(this.keepDef), ...mapValues(this.replaceDef)]) {
for (const def of [...mapValues(this.keepDef), ...mapValues(this.mockDef), ...mapValues(this.replaceDef)]) {
if (!isNgDef(def, 'm')) {
continue;
}
Expand Down Expand Up @@ -213,25 +219,43 @@ 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 (ngMocksUniverse.config.get('depsSkip').has(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 Expand Up @@ -301,9 +325,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
}

let meta: NgModule | undefined;
if (isNgDef(value, 'm')) {
meta = ngModuleResolver.resolve(value);
} else if (isNgDef(value, 'c')) {
if (isNgDef(value, 'c')) {
meta = directiveResolver.resolve(value);
} else if (isNgDef(value, 'd')) {
meta = directiveResolver.resolve(value);
Expand All @@ -316,7 +338,7 @@ export class MockBuilderPromise implements PromiseLike<IMockBuilderResult> {
if (!skipMock) {
ngMocksUniverse.flags.add('skipMock');
}
const [changed, def] = MockNgDef(meta);
const [changed, def] = MockNgDef({ providers: meta.providers });
/* istanbul ignore else */
if (!skipMock) {
ngMocksUniverse.flags.delete('skipMock');
Expand Down
8 changes: 2 additions & 6 deletions lib/mock-builder/mock-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ export function MockBuilder(
for (const [def, override] of mapEntries(overrides)) {
(TestBed as any).ngMocksOverrides.add(def);
/* istanbul ignore else */
if (isNgDef(def, 'm')) {
testBed.overrideModule(def, override);
} else if (isNgDef(def, 'c')) {
if (isNgDef(def, 'c')) {
testBed.overrideComponent(def, override);
} else if (isNgDef(def, 'd')) {
testBed.overrideDirective(def, override);
Expand All @@ -81,9 +79,7 @@ export function MockBuilder(
ngMocks.flushTestBed();
for (const def of (TestBed as any).ngMocksOverrides) {
/* istanbul ignore else */
if (isNgDef(def, 'm')) {
TestBed.overrideModule(def, {});
} else if (isNgDef(def, 'c')) {
if (isNgDef(def, 'c')) {
TestBed.overrideComponent(def, {});
} else if (isNgDef(def, 'd')) {
TestBed.overrideDirective(def, {});
Expand Down
Loading

0 comments on commit 0319543

Please sign in to comment.