不執行 ng serve 下就可以測試 route

前後端分離後,Angular 有自己的 route,在 component 也必須處理 route。由於 component 是 DI 注入 Router,因此若要對 route 單元測試,首先必須要解決的就是 Router 注入問題 。

Version


Node.js 8.9.4
Angular CLI 1.6.7
Angular 5.2.3

User Story


router000

首頁使用的是 PostComponent,按下 Redirect to comment 將會導到 /comment

router001

導到 /comment 之後,將顯示 CommentComponent

Task


PostComponent 單元測試,確定有正確導到 /comment

Implementation


app-routing.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PostComponent } from './post/post.component';
import { CommentComponent } from './comment/comment.component';

export const routes: Routes = [
{ path: '', component: PostComponent },
{ path: 'comment', component: CommentComponent }
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

第 6 行

1
2
3
4
export const routes: Routes = [
{ path: '', component: PostComponent },
{ path: 'comment', component: CommentComponent }
];

實際的 route 設定,為了單元測試也能使用,所以特別使用 export

post.component.html

1
2
<p>post works!</p>
<button (click)="onRedirectClick()">Redirect to comment</button>

當按下 Redirect to comment 時,會執行 onRedirectClick()

post.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Component } from '@angular/core';
import { Router } from '@angular/router';

@Component({
selector: 'app-post',
templateUrl: './post.component.html',
styleUrls: ['./post.component.css']
})
export class PostComponent {
constructor(private router: Router) {
}

onRedirectClick() {
this.router.navigateByUrl('comment');
}
}

10 行

1
2
constructor(private router: Router) {
}

要使用 Router 去切換 route,因此 DI 注入 Router

13 行

1
2
3
onRedirectClick() {
this.router.navigateByUrl('comment');
}

使用 Router.navigateByUrl() 切換到 comment

post.component.spec.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { PostComponent } from './post.component';
import { RouterTestingModule } from '@angular/router/testing';
import { routes } from '../app-routing.module';
import { CommentComponent } from '../comment/comment.component';
import { Router } from '@angular/router';

describe('PostComponent', () => {
let fixture: ComponentFixture<PostComponent>;
let postComponent: PostComponent;
let router: Router;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes(routes)
],
declarations: [
PostComponent,
CommentComponent
]
});
fixture = TestBed.createComponent(PostComponent);
postComponent = fixture.componentInstance;
fixture.detectChanges();

router = TestBed.get(Router);
});

it('should create', () => {
expect(postComponent).toBeTruthy();
});

it(`should navigate to comment`, fakeAsync(() => {
/** act */
postComponent.onRedirectClick();
tick();

/** assert */
expect(router.url).toEqual('/comment');
}));
});

15 行

1
2
3
imports: [
RouterTestingModule.withRoutes(routes)
],

Angular 對於 route 單元測試,另外提供了 RouterTestingModule,使用 withRoutes() 將原本的 route 帶入。

18 行

1
2
3
4
declarations: [
PostComponent,
CommentComponent
]

由於我們由 RouterTestingModule.withRoutes() 所載入的 route 包含 PostComponentCommentComponent,因此需要特別在 declarations 宣告。

11 行

1
let router: Router;

宣告要測試的 router

由於其型別仍然是 Router,而不是如 HttpClientHttpTestingController,所以不算 mock,RouterTestingModule 只是幫我們解決了 DI 的問題

27 行

1
router = TestBed.get(Router);

由 DI 幫我們建立 router 物件。

34 行

1
2
3
4
5
6
7
8
it(`should navigate to comment`, fakeAsync(() => {
/** act */
postComponent.onRedirectClick();
tick();

/** assert */
expect(router.url).toEqual('/comment');
}));

這裡使用了 fackAsync()tick(),這是 Angular 提供的非同步測試機制。

主要的原因是 Router.navigateByUrl() 回傳的是 Promise<boolean>,也就是非同步,因此必須使用 fackAsync()tick()

簡單的說,由於在 onRedirectClick()Router.navigateByUrl() 為非同步,我們必須使用 tick() 等待非同步執行完畢後,才能繼續執行 expect()tick() 讓我們不用寫 callback,以更同步的寫法來測試非同步行為。

最後直接 expect() router.url,測試目前 url 有沒有如預期。

router002

使用 Wallaby.js 通過所有 route 單元測試。

Conclusion


  • Route 單元測試的可貴在於不用執行 ng serve,就可以測試 route 是否正確
  • Route 單元測試的難點在於該如何解決 Router DI 注入,所幸搭配 RouterTestingModule,就可以幫我們解決
  • RouterTestingModule 所提供的方式不算 mock Router,只是幫我們解決了 Router DI 注入問題,不管如何,能夠在不執行 ng serve 就可執行測試,就非常難得

Sample Code


完整的範例可以在我的 GitHub 上找到

Reference


Angular, Testing
Asim, Testing Routing
Burak Tasci, Using Jasmine framework to test Angular Router

2018-02-02