51Testing软件测试论坛

标题: 如何对 Service 单元测试 ? [打印本页]

作者: 恭喜发财dife    时间: 2018-1-30 11:52
标题: 如何对 Service 单元测试 ?
如何对 Service [url=]单元测试[/url] ?
  使用 HttpTestingController 将大幅简化单元测试
  Contents
  1.Version
  2.User Story
  3.Task
  4.Architecture
  5.Implementation
  1.PostService
  2.AppComponent
  6.Conclusion
  7.Sample Code
  8.Reference
  凡与显示相关逻辑,我们会写在 component;凡与资料相关逻辑,我们会写在 service。而
service 最常见的应用,就是透过HttpClient?存取 API。
  对于 service 单元测试而言,我们必须对?HttpClient?加以隔离;而对 component 单元测试
而言,我们必须对?service?加以隔离,我们该如何对 service 与 component 进行单元测试呢?
  Version
  Angular CLI 1.6.2
  Node.js 8.9.4
  Angular 5.2.2
  User Story

  ●Header 会显示Welcome to app!
  ●程序一开始会显示所有 post
  ●按Add Post会呼叫POST API 新增 post
  ●按List Posts会呼叫GET API 回传 所有 post
  Task
  ●目前有PostService使用HttpClient存取 API,为了对?PostService?做?单元测试,必须对
HttpClient加以隔离
  ●目前有AppComponent使用PostService, 为了对?AppComponent?做?单元测试,必须
对PostService加以隔离
       Architecture

  ●AppComponent负责新增 post与显示 post?的界面显示;而PostService?负责 API 的串接
  ●根据依赖反转原则,AppComponent不应该直接相依于PostService,而是两者相依于
interface
  ●根据界面隔离原则,AppComponent只相依于它所需要的 interface,因此以
AppComponent的角度订出PostInterface,且PostService必须实作此 interface
  ●因为AppComponent与PostService都相依于PostInterface,两者都只知道PostInterface?
而已,而不知道彼此,因此AppComponent与PostService彻底解耦合
  ●透过 DI 将实作PostInterface的PostService注入到AppComponent,且将?HttpClient注
入到PostService
  Implementation
  AppComponent与?PostService的实作并非本文的重点,本文的重点在于实作?
AppComponent与PostService的单元测试部分。
  PostService

  post.service.spec.ts
  1.   import { TestBed } from '@angular/core/testing';
  2.   import { PostService } from './post.service';
  3.   import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
  4.   import { PostInterfaceToken } from '../interface/injection.token';
  5.   import { Post } from '../../model/post.model';
  6.   import { environment } from '../../environments/environment';
  7.   import { PostInterface } from '../interface/post.interface';
  8.   describe('PostService', () => {
  9.     let postService: PostInterface;
  10.     let mockHttpClient: HttpTestingController;
  11.     beforeEach(() => {
  12.       TestBed.configureTestingModule({
  13.         imports: [
  14.           HttpClientTestingModule
  15.         ],
  16.         providers: [
  17.           {provide: PostInterfaceToken, useClass: PostService}
  18.         ]
  19.       });
  20.       postService = TestBed.get(PostInterfaceToken, PostService);
  21.       mockHttpClient = TestBed.get(HttpTestingController);
  22.     });
  23.     it('should be created', () => {
  24.       expect(PostService).toBeTruthy();
  25.     });
  26.     it(`should list all posts`, () => {
  27.       /** act */
  28.       const expected: Post[] = [
  29.         {
  30.           id: 1,
  31.           title: 'Design Pattern',
  32.           author: 'Dr. Eric Gamma'
  33.         }
  34.       ];
  35.       postService.listPosts$().subscribe(posts => {
  36.         /** assert */
  37.         expect(posts).toEqual(expected);
  38.       });
  39.       /** arrange */
  40.       const mockResponse: Post[] = [
  41.         {
  42.           id: 1,
  43.           title: 'Design Pattern',
  44.           author: 'Eric Gamma'
  45.         }
  46.       ];
  47.       mockHttpClient.expectOne({
  48.         url: `${environment.apiServer}/posts`,
  49.         method: 'GET'
  50.       }).flush(mockResponse);
  51.     });
  52.     it(`should add post`, () => {
  53.       /** act */
  54.       const expected: Post = {
  55.         id: 1,
  56.         title: 'OOP',
  57.         author: 'Sam'
  58.       };
  59.       postService.addPost(expected).subscribe(post => {
  60.         /** assert */
  61.         expect(post).toBe(expected);
  62.       });
  63.       /** arrange */
  64.       mockHttpClient.expectOne({
  65.         url: `${environment.apiServer}/posts`,
  66.         method: 'POST'
  67.       }).flush(expected);
  68.     });
  69.     afterEach(() => {
  70.       mockHttpClient.verify();
  71.     });
  72.   });
复制代码
14行
  1. TestBed.configureTestingModule({
  2.     imports: [
  3.       HttpClientTestingModule
  4.     ],
  5.     providers: [
  6.       {provide: PostInterfaceToken, useClass: PostService}
  7.     ]
  8.   });
复制代码
Angular 有 module 观念,若使用到了其他 module,必须在imports设定;若使用到 DI,则必须在providers设定。
  若只有一个 module,则在AppModule设定。
  但是单元测试时,并没有使用AppModule的设定,因为我们可能在测试时使用其他替代 module,也可能自己实作 fake 另外 DI。
  Angular 提供了TestBed.configureTestingModule(),让我们另外设定跑测试时的imports与providers部分。

15行
  1.  imports: [
  2.     HttpClientTestingModule
  3.   ],
复制代码
原本HttpClient使用的是HttpClientModule,这会使得HttpClient真的透过网络去打 API,

这就不符合单元测试隔离的要求,因此 Angular 另外提供HttpClientTestingModule取代
HttpClientModule。
     18 行
  1.  providers: [
  2.     {provide: PostInterfaceToken, useClass: PostService}
  3.   ]
复制代码
由于我们要测试的就是PostService,因此PostService?也必须由 DI 帮我们建立。
  但因爲?PostService?是基于PostInterface?建立,因此必须透过PostInterfaceToken?
mapping 到?PostService。
      23 行

  1. postService = TestBed.get(PostInterfaceToken, PostService);
  2.   mockHttpClient = TestBed.get(HttpTestingController);
复制代码
由providers设定好 interface 与 class 的 mapping 关系后,我们必须透过 DI 建立
postService与mockHttpClient。
  其中HttpTestingController相当于 mock 版的?HttpClient,因此取名为mockHttpClient。
  TestBed.get() 其实相当于new,只是这是藉由 DI 帮我们new?而已




作者: 恭喜发财dife    时间: 2018-1-30 11:57
31 行
  1.  it(`should list all posts`, () => {
  2.     /** act */
  3.     const expected: Post[] = [
  4.       {
  5.         id: 1,
  6.         title: 'Design Pattern',
  7.         author: 'Dr. Eric Gamma'
  8.       }
  9.     ];
  10.     postService.listPosts$().subscribe(posts => {
  11.       /** assert */
  12.       expect(posts).toEqual(expected);
  13.     });
  14.     /** arrange */
  15.     const mockResponse: Post[] = [
  16.       {
  17.         id: 1,
  18.         title: 'Design Pattern',
  19.         author: 'Eric Gamma'
  20.       }
  21.     ];
  22.     mockHttpClient.expectOne({
  23.       url: `${environment.apiServer}/posts`,
  24.       method: 'GET'
  25.     }).flush(mockResponse);
  26.   });
复制代码

直接对PostService.listPost$()测试,由于listPost$()?回传?Observable,因此expect()必须写在subscribe()内。
  将预期的测试结果写在expected内。
  一般Observable会在subscribe()后执行,不过在HttpTestingController的设计里,
subscribe()会在flush()才执行,稍后会看到flush(),所以此时并还没有执行expect()测试
  46 行
  1. /** arrange */
  2.   const mockResponse: Post[] = [
  3.     {
  4.       id: 1,
  5.       title: 'Design Pattern',
  6.       author: 'Eric Gamma'
  7.     }
  8.   ];
  9.   mockHttpClient.expectOne({
  10.     url: `${environment.apiServer}/posts`,
  11.     method: 'GET'
  12.   }).flush(mockResponse);
复制代码
之前已经使用HttpClientTestingModule取代HttpClient,HttpTestingController取代
HttpClient,这只能确保呼叫 API 时不用透过网络。
  还要透过expectOne()设定要 mock 的 URI 与 action,之所以取名为?expectOne(),就是
期望有人真的呼叫这个 URI一次,且必须为GET,若没有呼叫这个 URI 或者不是GET,将造成单元
测试红灯。
  这也是为什么HttpTestingController的设计是act与assert要先写,最后再写?arrange,因为
HttpTestingController本身也有assert功能,必须有act,才能知道assert?URI 与 GET 有没有错误。
  最后使用flush()设定 mock 的回传值,flush英文就是冲水,当HttpTestingController将
mockResponse冲出去后,才会执行subscribe()内的expect()测试。
  也就是说若你忘了写flush(),其实单元测试也会?绿灯,但此时的绿灯并不是真的测试通过,
而是根本没有执行到subscribe()内的expect()。
  81 行

  1. afterEach(() => {
  2.     mockHttpClient.verify();
  3.   });
复制代码
实务上可能真的忘了写expectOne()与flush(),导致subscribe()内的expect()根本没跑到而
造成单元测试绿灯,因此必须在每个单元测试跑完补上?mockHttpClient.verify(),若有任何 API
request 却没有经过expectOne()?与?flush()?测试,则?verify()?会造成单元测试红灯,借以弥补
忘了写?expectOne()?与?flush()?的人为错误。
  Q : 我们在?listPosts$()?的单元测试到底测试了什么 ?
  若 service 的 API 与 mock 不同,会出现单元测试红灯,可能是 service 的 API 错误
  若 service 的 action 与 mock 不同,会出现单元测试红灯,可能是 service 的 action 错误
  若 service 的 response 与 expected 不同,可能是 service 的逻辑错误
  61 行
  1. it(`should add post`, () => {
  2.       /** act */
  3.       const expected: Post = {
  4.         id: 1,
  5.         title: 'OOP',
  6.         author: 'Sam'
  7.       };
  8.       postService.addPost(expected).subscribe(post => {
  9.         /** assert */
  10.         expect(post).toBe(expected);
  11.       });
  12.       /** arrange */
  13.       mockHttpClient.expectOne({
  14.         url: `${environment.apiServer}/posts`,
  15.         method: 'POST'
  16.       }).flush(expected);
  17.     });
复制代码
再来谈谈如何测试POST。
  62 行
  1. /** act */
  2.   const expected: Post = {
  3.     id: 1,
  4.     title: 'OOP',
  5.     author: 'Sam'
  6.   };
  7.   postService.addPost(expected).subscribe(post => {
  8.     /** assert */
  9.     expect(post).toBe(expected);
  10.   });
复制代码
直接对?PostService.addPost()?测试,由于addPost()?回传?Observable,因此expect()?必
须写在?subscribe()?内。
  将预期的测试结果写在?expected?内。
  74 行
  1. /** arrange */
  2.   mockHttpClient.expectOne({
  3.     url: `${environment.apiServer}/posts`,
  4.     method: 'POST'
  5.   }).flush(expected);
复制代码
因为要 mock?POST,因此method?部分改为POST,其他部分与?GET?部分完全相同。
  Q : 我们在?addPost()?到底测试了什么 ?
  ●若 service 的 API 与 mock 不同,会出现单元测试红灯,可能是 service 的 API 错误
  ●若 service 的 action 与 mock 不同,会出现单元测试红灯,可能是 service 的 action 错误
  ●若 service 的 response 与 expected 不同,可能是 service 的逻辑错误

  使用 Wallaby.js 通过所有 service 单元测试。









欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2