AppComponent
app.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { FormsModule } from '@angular/forms'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { PostService } from './service/post.service'; import { PostInterfaceToken } from './interface/injection.token'; import { DebugElement } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; import { PostInterface } from './interface/post.interface'; describe('AppComponent', () => { let fixture: ComponentFixture<AppComponent>; let appComponent: AppComponent; let debugElement: DebugElement; let htmlElement: HTMLElement; let postService: PostInterface; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], imports: [ FormsModule, HttpClientTestingModule ], providers: [ {provide: PostInterfaceToken, useClass: PostService} ] }).compileComponents(); fixture = TestBed.createComponent(AppComponent); appComponent = fixture.componentInstance; debugElement = fixture.debugElement; htmlElement = debugElement.nativeElement; fixture.detectChanges(); postService = TestBed.get(PostInterfaceToken, PostService); })); it('should create the app', async(() => { expect(appComponent).toBeTruthy(); })); it(`should have as title 'app'`, async(() => { expect(appComponent.title).toEqual('app'); })); it('should render title in a h1 tag', async(() => { expect(htmlElement.querySelector('h1').textContent).toContain('Welcome to app!'); })); it(`should list posts`, () => { const expected$ = Observable.of([ { id: 1, title: 'Design Pattern', author: 'Dr. Eric Gamma' } ]); /** arrange */ spyOn(postService, 'listPosts$').and.returnValue(expected$); /** act */ appComponent.onListPostsClick(); /** assert */ expect(appComponent.posts$).toEqual(expected$); }); it(`should add post`, () => { const expected$ = Observable.of( { id: 1, title: 'Design Pattern', author: 'Dr. Eric Gamma' } ); /** arrange */ const spy = spyOn(postService, 'addPost').and.returnValue(expected$); /** act */ appComponent.onAddPostClick(); /** assert */ expect(spy).toHaveBeenCalled(); }); }); |
20 行
TestBed.configureTestingModule({ declarations: [ AppComponent ], imports: [ FormsModule, HttpClientTestingModule ], providers: [ {provide: PostInterfaceToken, useClass: PostService} ] }).compileComponents(); |
跑 component 单元测试时,一样没有使用AppModule?的设定,因为我们可能在测试时使用其他替代 module,也可能自己实作 fake 另外 DI。
因此一样使用TestBed.configureTestingModule(),让我们另外设定跑测试时的?imports?与?providers?部分。
24 行
imports: [ FormsModule, HttpClientTestingModule ], |
因为在 component 使用了 two-way binding,因此要加上FormsModule。
Q : 为什么要 import?HttpClientTestingModule?呢 ?
AppComponent?依赖的是?PostService,看起来与?HttpClient?无关,应该不需要注入HttpClientTestingModule。
但其实 DI 并不是这样运作,虽然AppComponent?只用到了?PostService,但 DI 会将?PostService?下所有的 dependency 都一起注入,所以也要 import?HttpClientTestingModule。
28 行
providers: [ {provide: PostInterfaceToken, useClass: PostService} ] |
由于我们要测试的就是PostService,因此PostService?也必须由 DI 帮我们建立。
但因爲?PostService?是基于PostInterface?建立,因此必须透过PostInterfaceToken?mapping 到?PostService。
39 行
1 postService = TestBed.get(PostInterfaceToken, PostService);
由providers设定好 interface 与 class 的 mapping 关系后,我们必须透过 DI 建立postService?与。
53 行
it(`should list posts`, () => { const expected$ = Observable.of([ { id: 1, title: 'Design Pattern', author: 'Dr. Eric Gamma' } ]); /** arrange */ spyOn(postService, 'listPosts$').and.returnValue(expected$); /** act */ appComponent.onListPostsClick(); /** assert */ expect(appComponent.posts$).toEqual(expected$); }); |
55 行
const expected$ = Observable.of([ { id: 1, title: 'Design Pattern', author: 'Dr. Eric Gamma' } ]); 透过Observable.of()?将?Post[]?转成?Observable<Post[]>。 |
63 行
/** arrange */ spyOn(postService, 'listPosts$').and.returnValue(expected$); |
由于我们要隔离PostService,因此使用spyOn()?对?PostService?的?listPost$()?加以 mock,并设定其假的回传值。
65 行
/** act */ appComponent.onListPostsClick(); |
实际测试onListPostClick()。
67 行
/** assert */ expect(appComponent.posts$).toEqual(expected$); |
测试AppComponent.post$?是否如预期。
Q : 我们在onListPostsClick()到底测试了什么 ?
●若 component 的 return 与 expected 不同,可能是 component 的逻辑错误
70 行
it(`should add post`, () => { const expected$ = Observable.of( { id: 1, title: 'Design Pattern', author: 'Dr. Eric Gamma' } ); /** arrange */ const spy = spyOn(postService, 'addPost').and.returnValue(expected$); /** act */ appComponent.onAddPostClick(); /** assert */ expect(spy).toHaveBeenCalled(); }); |
72 行
const expected$ = Observable.of( { id: 1, title: 'Design Pattern', author: 'Dr. Eric Gamma' } ); |
透过Observable.of()?将Post转成Observable<Post>。
80 行
/** arrange */ const spy = spyOn(postService, 'addPost').and.returnValue(expected$); |
由于我们要隔离PostService,因此使用spyOn()对PostService的addPost加以 mock,并设定其假的回传值。
82 行
/** act */ appComponent.onAddPostClick(); |
实际测试onAddPostClick()。
84 行
/** assert */ expect(spy).toHaveBeenCalled(); |
由于onAddPostClick()回传值为void,且PostService.addPost()已经有单元测试保护,因此只要测试PostService.addPost()曾经被呼叫过即可。
使用 Wallaby.js 通过所有 component 单元测试。
Conclusion
当 component 使用了 service,若要单元测试就牵涉到 DI 与spyOn()
Service 单元测试可透过HttpTestingController加以隔离HttpClient
Component 单元测试可透过spyOn()加以隔离 service
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。