From fce5b2747aed3e532a7408a89d8aa22beea5f247 Mon Sep 17 00:00:00 2001 From: MG Date: Mon, 26 Oct 2020 13:43:31 +0100 Subject: [PATCH] docs: how to test a route --- README.md | 202 +++++++++++++------------------- examples/TestRoute/test.spec.ts | 42 ++++++- 2 files changed, 120 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index ac4e92d2f0..3ecce75474 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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: @@ -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: ` - 1 - 2 - - `, -}) -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 diff --git a/examples/TestRoute/test.spec.ts b/examples/TestRoute/test.spec.ts index d7d781ee44..de3c8c2a11 100644 --- a/examples/TestRoute/test.spec.ts +++ b/examples/TestRoute/test.spec.ts @@ -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'; @@ -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