Skip to content

Commit

Permalink
docs: how to test a route
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Oct 26, 2020
1 parent 9813e81 commit fce5b27
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 124 deletions.
202 changes: 80 additions & 122 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2148,7 +2148,7 @@ The source file of this test is here:

### How to test a provider of a directive

This test is quite similar to [testing a provider of a component](#how-to-test-a-provider-of-a-component).
This test is quite similar to ["How to test a provider of a component"](#how-to-test-a-provider-of-a-component).
With difference that we need a bit different template.

Let's prepare `TestBed`: the service for testing is the first parameter, the directive is the second one:
Expand Down Expand Up @@ -2218,7 +2218,7 @@ The source file of this test is here:

### How to test a structural directive with context

If you did not read [testing of structural directives](#how-to-test-a-structural-directive), please do it first.
If you did not read ["How to test a structural directive"](#how-to-test-a-structural-directive), please do it first.

The difference for structural directives with context in terms of testing is what we want to render in a custom template.
It should include variables:
Expand Down Expand Up @@ -2402,142 +2402,100 @@ The source file of this test is here:

### How to test a route

The source file is here:
[examples/TestRoute/test.spec.ts](https://github.com/ike18t/ng-mocks/blob/master/examples/TestRoute/test.spec.ts)
Testing a route means that we want to assert that a specific page renders a specific component.

With `ngMocks` you can be confident, that a route exists and all its dependencies are present in the related module,
otherwise tests will fail.

However, to test that, we need to configure `TestBed` a bit differently: it is fine to mock all components and declarations,
we should only keep the `RouterModule` as it is, and to add `RouterTestingModule` with empty routes.
This guarantees that the application routes will be used, and tests fail if a route or its dependencies have been removed.

```typescript
import { Location } from '@angular/common';
import { Component, NgModule } from '@angular/core';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { Router, RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';
beforeEach(() =>
MockBuilder(RouterModule, TargetModule).keep(
RouterTestingModule.withRoutes([])
)
);
```

// A layout component that renders the current route.
@Component({
selector: 'target',
template: `
<a routerLink="/1">1</a>
<a routerLink="/2">2</a>
<router-outlet></router-outlet>
`,
})
class TargetComponent {}
The next and very import step is to wrap a test callback in `it` with `fakeAsync` function and to render `RouterOutlet`.
We need this, because `RouterModule` relies on async zones.

// A simple component for the first route.
@Component({
selector: 'target1',
template: 'target1',
})
class Target1Component {}
```typescript
// fakeAsync --------------------------|||||||||
it('renders /1 with Target1Component', fakeAsync(() => {
const fixture = MockRender(RouterOutlet);
}));
```

// A simple component for the second route.
@Component({
selector: 'target2',
template: 'target2',
})
class Target2Component {}
After we have rendered `RouterOutlet` we should initialize the router, also we can set the default url here.
As mentioned above, we should use zones and `fakeAsync` for that.

// Definition of the routing module.
@NgModule({
declarations: [Target1Component, Target2Component],
exports: [RouterModule],
imports: [
RouterModule.forRoot([
{
component: Target1Component,
path: '1',
},
{
component: Target2Component,
path: '2',
},
]),
],
})
class TargetRoutingModule {}
```typescript
const router: Router = TestBed.get(Router);
const location: Location = TestBed.get(Location);

// Definition of the main module.
@NgModule({
declarations: [TargetComponent],
exports: [TargetComponent],
imports: [TargetRoutingModule],
})
class TargetModule {}
location.go('/1');
if (fixture.ngZone) {
fixture.ngZone.run(() => router.initialNavigation());
tick();
}
```

describe('TestRoute', () => {
// Because we want to test navigation, it means that we want to
// test a component with 'router-outlet' and its integration with
// RouterModule. Therefore, we pass the component as the first
// parameter of MockBuilder. Then, to correctly satisfy its
// initialization, we need to pass its module as the second
// parameter. And, the last but not the least, we need avoid
// mocking of RouterModule to have its routes, and to add
// RouterTestingModule.withRoutes([]), yes yes, with empty routes
// to have tools for testing.
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule)
.keep(RouterModule)
.keep(RouterTestingModule.withRoutes([]))
);
Now we can assert the current route and what it has rendered.

it('navigates between pages', fakeAsync(() => {
const fixture = MockRender(TargetComponent);
const router: Router = TestBed.get(Router);
const location: Location = TestBed.get(Location);
```typescript
expect(location.path()).toEqual('/1');
expect(() => ngMocks.find(fixture, Target1Component)).not.toThrow();
```

// First we need to initialize navigation.
if (fixture.ngZone) {
fixture.ngZone.run(() => router.initialNavigation());
tick(); // is needed for rendering of the current route.
}
That's it.

// By default our routes do not have a component.
// Therefore non of them should be rendered.
expect(location.path()).toEqual('/');
expect(() => ngMocks.find(fixture, Target1Component)).toThrow();
expect(() => ngMocks.find(fixture, Target2Component)).toThrow();
Additionally, we might assert that a link on a page navigates to the right route.
In this case, we should pass a component of the link as the first parameter to [`MockBuilder`](#mockbuilder),
to `.keep` `RouterModule` and to render the component instead of `RouterOutlet`.

// Let's extract our navigation links.
const links = ngMocks.findAll(fixture, 'a');
```typescript
beforeEach(() =>
MockBuilder(TargetComponent, TargetModule)
.keep(RouterModule)
.keep(RouterTestingModule.withRoutes([]))
);
```

// Checking where we land if we click the first link.
if (fixture.ngZone) {
fixture.ngZone.run(() => {
// To simulate a correct click we need to let the router know
// that it is the left mouse button via setting its parameter
// to 0 (not undefined, null or anything else).
links[0].triggerEventHandler('click', {
button: 0,
});
});
tick(); // is needed for rendering of the current route.
}
// We should see Target1Component component on /1 page.
expect(location.path()).toEqual('/1');
expect(() =>
ngMocks.find(fixture, Target1Component)
).not.toThrow();
```typescript
it('navigates between pages', fakeAsync(() => {
const fixture = MockRender(TargetComponent);
}));
```

// Checking where we land if we click the second link.
if (fixture.ngZone) {
fixture.ngZone.run(() => {
// A click of the left mouse button.
links[1].triggerEventHandler('click', {
button: 0,
});
});
tick(); // is needed for rendering of the current route.
}
// We should see Target2Component component on /2 page.
expect(location.path()).toEqual('/2');
expect(() =>
ngMocks.find(fixture, Target2Component)
).not.toThrow();
}));
});
The next step is to find the link we want to click. The click event should be inside of zones, because it triggers navigation.
Please notice, that `button: 0` should be sent with the event to simulate the left button click.

```typescript
const links = ngMocks.findAll(fixture, 'a');
if (fixture.ngZone) {
fixture.ngZone.run(() => {
links[0].triggerEventHandler('click', {
button: 0, // <- simulating the left button click, not right one.
});
});
tick();
}
```

Now we can assert the current state: the location should be changed to the expected route, and the fixture should contain
its component.

```typescript
expect(location.path()).toEqual('/1');
expect(() => ngMocks.find(fixture, Target1Component)).not.toThrow();
```

The source file of these tests is here:
[examples/TestRoute/test.spec.ts](https://github.com/ike18t/ng-mocks/blob/master/examples/TestRoute/test.spec.ts)

---

### How to test a routing guard
Expand Down
42 changes: 40 additions & 2 deletions examples/TestRoute/test.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Location } from '@angular/common';
import { Component, NgModule } from '@angular/core';
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { Router, RouterModule } from '@angular/router';
import { Router, RouterModule, RouterOutlet } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';

Expand Down Expand Up @@ -57,7 +57,45 @@ class TargetRoutingModule {}
})
class TargetModule {}

describe('TestRoute', () => {
describe('TestRoute:Route', () => {
beforeEach(() => MockBuilder(RouterModule, TargetModule).keep(RouterTestingModule.withRoutes([])));

it('renders /1 with Target1Component', fakeAsync(() => {
const fixture = MockRender(RouterOutlet);
const router: Router = TestBed.get(Router);
const location: Location = TestBed.get(Location);

// First we need to initialize navigation.
location.go('/1');
if (fixture.ngZone) {
fixture.ngZone.run(() => router.initialNavigation());
tick(); // is needed for rendering of the current route.
}

// We should see Target1Component component on /1 page.
expect(location.path()).toEqual('/1');
expect(() => ngMocks.find(fixture, Target1Component)).not.toThrow();
}));

it('renders /2 with Target2Component', fakeAsync(() => {
const fixture = MockRender(RouterOutlet);
const router: Router = TestBed.get(Router);
const location: Location = TestBed.get(Location);

// First we need to initialize navigation.
location.go('/2');
if (fixture.ngZone) {
fixture.ngZone.run(() => router.initialNavigation());
tick(); // is needed for rendering of the current route.
}

// We should see Target2Component component on /2 page.
expect(location.path()).toEqual('/2');
expect(() => ngMocks.find(fixture, Target2Component)).not.toThrow();
}));
});

describe('TestRoute:Component', () => {
// Because we want to test navigation, it means that we want to
// test a component with 'router-outlet' and its integration with
// RouterModule. Therefore, we pass the component as the first
Expand Down

0 comments on commit fce5b27

Please sign in to comment.