Skip to content

Commit

Permalink
feat(mock-render): renders everything
Browse files Browse the repository at this point in the history
closes #266
  • Loading branch information
satanTime committed Jan 1, 2021
1 parent b9b068c commit f33a132
Show file tree
Hide file tree
Showing 47 changed files with 664 additions and 294 deletions.
118 changes: 89 additions & 29 deletions README.md
Expand Up @@ -174,8 +174,7 @@ TestBed.configureTestingModule({
// ...
],
providers: [
LoginService,
DataService,
SearchService,
// ...
],
});
Expand Down Expand Up @@ -203,14 +202,36 @@ TestBed.configureTestingModule({
// ...
],
providers: [
MockProvider(LoginService),
MockProvider(DataService),
MockProvider(SearchService),
// ...
],
});
```

Profit. Now we can forget about noise of child dependencies.
If you have noticed `search$ | async` in the template, you made the right assumption:
we need to provide a fake observable stream within the mock `SearchService` to avoid failures
like [`Cannot read property 'pipe' of undefined`](#how-to-fix-typeerror-cannot-read-property-subscribe-of-undefined),
when the component tries to execute `this.search$ = this.searchService.result$.pipe(...)` in `ngOnInit`.

For example, we can implement it via [`MockInstance`](#mockinstance):

```ts
beforeEach(() =>
MockInstance(SearchService, () => ({
result$: EMPTY,
})),
);
```

or to set it as default mock behavior for all tests via adding [`ngMocks.defaultMock`](#ngmocksdefaultmock) in the `test.ts` file:

```ts
ngMocks.defaultMock(SearchService, () => ({
result$: EMPTY,
}));
```

Profit. Now, we can forget about noise of child dependencies.

Nevertheless, if we count lines of mock declarations,
we see that there are a lot of them, and looks like here might be dozens more for big
Expand Down Expand Up @@ -274,7 +295,7 @@ beforeEach(() => {
return MockBuilder(AppBaseComponent, AppBaseModule)
.mock(TranslatePipe, v => `translated:${v}`)
.mock(SearchService, {
search: of([]),
result$: EMPTY,
});
});
```
Expand Down Expand Up @@ -331,14 +352,14 @@ a type of `MockedComponent<T>` and provides:

- the same `selector`
- the same `Inputs` and `Outputs` with alias support
- templates are pure `ng-content` tags to allow transclusion
- supports `@ContentChild` with an `$implicit` context
- templates with pure `ng-content` tags to allow transclusion
- support of `@ContentChild` with an `$implicit` context
- `__render('id', $implicit, variables)` - renders a template
- `__hide('id')` - hides a rendered template
- supports [`FormsModule`, `ReactiveFormsModule` and `ControlValueAccessor`](#how-to-mock-form-controls)
- support of [`FormsModule`, `ReactiveFormsModule` and `ControlValueAccessor`](#how-to-mock-form-controls)
- `__simulateChange()` - calls `onChanged` on the mock component bound to a `FormControl`
- `__simulateTouch()` - calls `onTouched` on the mock component bound to a `FormControl`
- supports `exportAs`
- support of `exportAs`

Let's pretend that in our Angular application `TargetComponent` depends on a child component of `DependencyComponent`
and we want to use its mock object in a test.
Expand Down Expand Up @@ -972,17 +993,14 @@ describe('MockProvider', () => {
it('uses mock providers', () => {
// overriding the token's data that does affect the provided token.
mockObj.value = 321;
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
const fixture = MockRender(TargetComponent);
expect(
fixture.debugElement.injector.get(Dependency1Service).echo(),
fixture.point.injector.get(Dependency1Service).echo(),
).toBeUndefined();
expect(
fixture.debugElement.injector.get(Dependency2Service).echo(),
fixture.point.injector.get(Dependency2Service).echo(),
).toBeUndefined();
expect(fixture.debugElement.injector.get(OBJ_TOKEN)).toBe(
mockObj,
);
expect(fixture.point.injector.get(OBJ_TOKEN)).toBe(mockObj);
expect(fixture.nativeElement.innerHTML).not.toContain('"target"');
expect(fixture.nativeElement.innerHTML).toContain('"d2:mock"');
expect(fixture.nativeElement.innerHTML).toContain('"mock token"');
Expand Down Expand Up @@ -1290,8 +1308,12 @@ describe('MockObservable', () => {

// Checking that a sibling method has been replaced
// with a mock object too.
expect(TestBed.inject(TargetService).getValue$).toBeDefined();
expect(TestBed.inject(TargetService).getValue$()).toBeUndefined();
expect(
fixture.point.injector.get(TargetService).getValue$,
).toBeDefined();
expect(
fixture.point.injector.get(TargetService).getValue$(),
).toBeUndefined();
});
});
```
Expand Down Expand Up @@ -2137,11 +2159,16 @@ beforeEach(() => {

### MockRender

`MockRender` is a simple tool that helps with **shallow rendering in Angular tests**
when we want to assert `Inputs`, `Outputs`, `ChildContent` and custom templates.
**Shallow rendering in Angular tests** is provided via `MockRender` function.
`MockRender` helps when we want to assert `Inputs`, `Outputs`, `ChildContent` and custom templates.

`MockRender` relies on Angular `TestBed` and provides:

The best thing about it is that `MockRender` properly triggers all lifecycle hooks
and allows **to test `ngOnChanges` hook from `OnChanges` interface**.
- shallow rendering of Components, Directives, Services, Tokens
- rendering of custom templates
- support of context providers
- support of all lifecycle hooks (`ngOnInit`, `ngOnChanges` etc)
- support of components without selectors

**Please note**, that `MockRender(Component)` is not assignable to
`ComponentFixture<Component>`. You should use either
Expand All @@ -2162,16 +2189,48 @@ It happens because `MockRender` generates an additional component to
render the desired thing and its interface differs.

It returns `MockedComponentFixture<T>` type. The difference is an additional `point` property.
The best thing about it is that `fixture.point.componentInstance` is typed to the component's class instead of `any`.
The best thing about it is that `fixture.point.componentInstance` is typed to the related class instead of `any`.

```ts
// component
const fixture = MockRender(AppComponent);
fixture.componentInstance; // is a middle component, mostly useless
fixture.point.componentInstance; // an instance of the AppComponent
```

```ts
// directive
const fixture = MockRender(AppDirective);
fixture.componentInstance; // is a middle component, mostly useless
fixture.point.componentInstance; // an instance of AppDirective
```

```ts
const fixture = MockRender(ComponentToRender);
// service
const fixture = MockRender(TranslationService);
fixture.componentInstance; // is a middle component, mostly useless
fixture.point.componentInstance; // an instance of TranslationService
```

```ts
// token
const fixture = MockRender(APP_BASE_HREF);
fixture.componentInstance; // is a middle component, mostly useless
fixture.point.componentInstance; // a value of APP_BASE_HREF
```

```ts
// custom template
const fixture = MockRender<AppComponent>(
`<app-component [header]="value | translate"></app-component`,
{ value: 'test' },
);
fixture.componentInstance; // is a middle component, mostly useless
fixture.point.componentInstance; // the thing we need
fixture.point.componentInstance; // an instance of AppComponent
```

If you want, you can specify providers for the render passing them via the 3rd parameter.
It is useful when you want to create mock system tokens / services such as `APP_INITIALIZER`, `DOCUMENT` etc.
It is useful when you want to provide mock system tokens / services such as `APP_INITIALIZER`, `DOCUMENT` etc.

```ts
const fixture = MockRender(
Expand All @@ -2189,8 +2248,8 @@ const fixture = MockRender(
);
```

And do not forget to call `fixture.detectChanges()` and / or `await fixture.whenStable()` to reflect changes in
the render.
Please, don't forget to call `fixture.detectChanges()` and / or `await fixture.whenStable()` to update the render
if you have changed values of parameters.

There is **an example how to render a custom template in an Angular test** below.

Expand Down Expand Up @@ -2812,6 +2871,7 @@ This function verifies how a class has been decorated.
- `isNgDef( SomeClass, 'd' )` - checks whether `SomeClass` is a directive
- `isNgDef( SomeClass, 'p' )` - checks whether `SomeClass` is a pipe
- `isNgDef( SomeClass, 'i' )` - checks whether `SomeClass` is a service
- `isNgDef( SomeClass, 't' )` - checks whether `SomeClass` is a token
- `isNgDef( SomeClass )` - checks whether `SomeClass` is a module / component / directive / pipe / service.

#### getSourceOfMock
Expand Down
9 changes: 6 additions & 3 deletions examples/MockObservable/test.spec.ts
@@ -1,6 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, Injectable, NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
MockBuilder,
MockInstance,
Expand Down Expand Up @@ -93,7 +92,11 @@ describe('MockObservable', () => {

// Checking that a sibling method has been replaced
// with a mock object too.
expect(TestBed.get(TargetService).getValue$).toBeDefined();
expect(TestBed.get(TargetService).getValue$()).toBeUndefined();
expect(
fixture.point.injector.get(TargetService).getValue$,
).toBeDefined();
expect(
fixture.point.injector.get(TargetService).getValue$(),
).toBeUndefined();
});
});
11 changes: 5 additions & 6 deletions examples/MockProvider/test.spec.ts
Expand Up @@ -6,7 +6,7 @@ import {
InjectionToken,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockProvider } from 'ng-mocks';
import { MockProvider, MockRender } from 'ng-mocks';

const UNK_TOKEN = new InjectionToken('UNK_TOKEN');
const STR_TOKEN = new InjectionToken<string>('STR_TOKEN');
Expand Down Expand Up @@ -72,15 +72,14 @@ describe('MockProvider', () => {
it('uses mock providers', () => {
// overriding the token's data that does affect the provided token.
mockObj.value = 321;
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
const fixture = MockRender(TargetComponent);
expect(
fixture.debugElement.injector.get(Dependency1Service).echo(),
fixture.point.injector.get(Dependency1Service).echo(),
).toBeUndefined();
expect(
fixture.debugElement.injector.get(Dependency2Service).echo(),
fixture.point.injector.get(Dependency2Service).echo(),
).toBeUndefined();
expect(fixture.debugElement.injector.get(OBJ_TOKEN)).toBe(
expect(fixture.point.injector.get(OBJ_TOKEN)).toBe(
mockObj as any,
);
expect(fixture.nativeElement.innerHTML).not.toContain('"target"');
Expand Down
14 changes: 10 additions & 4 deletions examples/TestHttpInterceptor/test.spec.ts
Expand Up @@ -12,8 +12,11 @@ import {
HttpTestingController,
} from '@angular/common/http/testing';
import { Injectable, NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder, NG_MOCKS_INTERCEPTORS } from 'ng-mocks';
import {
MockBuilder,
MockRender,
NG_MOCKS_INTERCEPTORS,
} from 'ng-mocks';
import { Observable } from 'rxjs';

// An interceptor we want to test.
Expand Down Expand Up @@ -88,8 +91,11 @@ describe('TestHttpInterceptor', () => {
});

it('triggers interceptor', () => {
const client: HttpClient = TestBed.get(HttpClient);
const httpMock: HttpTestingController = TestBed.get(
const fixture = MockRender('');
const client: HttpClient = fixture.debugElement.injector.get(
HttpClient,
);
const httpMock: HttpTestingController = fixture.debugElement.injector.get(
HttpTestingController,
);

Expand Down
10 changes: 6 additions & 4 deletions examples/TestHttpRequest/test.spec.ts
Expand Up @@ -4,8 +4,7 @@ import {
HttpTestingController,
} from '@angular/common/http/testing';
import { Injectable, NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder } from 'ng-mocks';
import { MockBuilder, MockRender } from 'ng-mocks';
import { Observable } from 'rxjs';

// A service that does http requests.
Expand Down Expand Up @@ -39,9 +38,12 @@ describe('TestHttpRequest', () => {
});

it('sends a request', () => {
const fixture = MockRender('');
// Let's extract the service and http controller for testing.
const service: TargetService = TestBed.get(TargetService);
const httpMock: HttpTestingController = TestBed.get(
const service: TargetService = fixture.debugElement.injector.get(
TargetService,
);
const httpMock: HttpTestingController = fixture.debugElement.injector.get(
HttpTestingController,
);

Expand Down
8 changes: 6 additions & 2 deletions examples/TestLifecycleHooks/test.spec.ts
Expand Up @@ -23,7 +23,9 @@ describe('TestLifecycleHooks', () => {
{ detectChanges: false },
);

const service: TargetService = TestBed.get(TargetService);
const service: TargetService = fixture.point.injector.get(
TargetService,
);

// By default nothing should be initialized, but ctor.
expect(service.ctor).toHaveBeenCalledTimes(1); // changed
Expand Down Expand Up @@ -95,7 +97,9 @@ describe('TestLifecycleHooks', () => {
const fixture = TestBed.createComponent(TargetComponent);
fixture.componentInstance.input = '';

const service: TargetService = TestBed.get(TargetService);
const service: TargetService = fixture.debugElement.injector.get(
TargetService,
);

// By default nothing should be initialized.
expect(service.onChanges).toHaveBeenCalledTimes(0);
Expand Down
5 changes: 3 additions & 2 deletions examples/TestLifecycleHooks/test.string.spec.ts
@@ -1,4 +1,3 @@
import { TestBed } from '@angular/core/testing';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';

import {
Expand All @@ -23,7 +22,9 @@ describe('TestLifecycleHooks:string', () => {
},
);

const service: TargetService = TestBed.get(TargetService);
const service: TargetService = fixture.point.injector.get(
TargetService,
);

// By default nothing should be initialized, but ctor.
expect(service.ctor).toHaveBeenCalledTimes(1); // changed
Expand Down
5 changes: 3 additions & 2 deletions examples/TestLifecycleHooks/test.type-without-params.spec.ts
@@ -1,4 +1,3 @@
import { TestBed } from '@angular/core/testing';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';

import {
Expand All @@ -19,7 +18,9 @@ describe('TestLifecycleHooks:type-without-params', () => {
detectChanges: false,
});

const service: TargetService = TestBed.get(TargetService);
const service: TargetService = fixture.point.injector.get(
TargetService,
);

// By default nothing should be initialized, but ctor.
expect(service.ctor).toHaveBeenCalledTimes(1); // changed
Expand Down
6 changes: 3 additions & 3 deletions examples/TestMultiToken/test.spec.ts
@@ -1,6 +1,5 @@
import { Injectable, InjectionToken, NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder } from 'ng-mocks';
import { MockBuilder, MockRender } from 'ng-mocks';

const TOKEN_MULTI = new InjectionToken('MULTI');

Expand Down Expand Up @@ -49,7 +48,8 @@ describe('TestMultiToken', () => {
beforeEach(() => MockBuilder(TOKEN_MULTI, TargetModule));

it('creates TOKEN_MULTI', () => {
const tokens = TestBed.get(TOKEN_MULTI);
const tokens = MockRender<any[]>(TOKEN_MULTI).point
.componentInstance;

expect(tokens).toEqual(jasmine.any(Array));
expect(tokens.length).toEqual(4);
Expand Down
5 changes: 2 additions & 3 deletions examples/TestProvider/test.spec.ts
@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { MockBuilder } from 'ng-mocks';
import { MockBuilder, MockRender } from 'ng-mocks';

// A simple service, might have contained more logic,
// but it is redundant for the test demonstration.
Expand All @@ -18,7 +17,7 @@ describe('TestProvider', () => {
beforeEach(() => MockBuilder(TargetService));

it('returns value on echo', () => {
const service = TestBed.get(TargetService);
const service = MockRender(TargetService).point.componentInstance;

expect(service.echo()).toEqual(service.value);
});
Expand Down

0 comments on commit f33a132

Please sign in to comment.