如何对 Service 单元测试 ?

发表于:2018-1-29 10:46

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:点灯坊    来源:个人博客

  如何对 Service 单元测试 ?
  使用 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
    import { TestBed } from '@angular/core/testing';
  import { PostService } from './post.service';
  import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
  import { PostInterfaceToken } from '../interface/injection.token';
  import { Post } from '../../model/post.model';
  import { environment } from '../../environments/environment';
  import { PostInterface } from '../interface/post.interface';
  describe('PostService', () => {
    let postService: PostInterface;
    let mockHttpClient: HttpTestingController;
    beforeEach(() => {
      TestBed.configureTestingModule({
        imports: [
          HttpClientTestingModule
        ],
        providers: [
          {provide: PostInterfaceToken, useClass: PostService}
        ]
      });
      postService = TestBed.get(PostInterfaceToken, PostService);
      mockHttpClient = TestBed.get(HttpTestingController);
    });
    it('should be created', () => {
      expect(PostService).toBeTruthy();
    });
    it(`should list all posts`, () => {
      /** act */
      const expected: Post[] = [
        {
          id: 1,
          title: 'Design Pattern',
          author: 'Dr. Eric Gamma'
        }
      ];
      postService.listPosts$().subscribe(posts => {
        /** assert */
        expect(posts).toEqual(expected);
      });
      /** arrange */
      const mockResponse: Post[] = [
        {
          id: 1,
          title: 'Design Pattern',
          author: 'Eric Gamma'
        }
      ];
      mockHttpClient.expectOne({
        url: `${environment.apiServer}/posts`,
        method: 'GET'
      }).flush(mockResponse);
    });
    it(`should add post`, () => {
      /** act */
      const expected: Post = {
        id: 1,
        title: 'OOP',
        author: 'Sam'
      };
      postService.addPost(expected).subscribe(post => {
        /** assert */
        expect(post).toBe(expected);
      });
      /** arrange */
      mockHttpClient.expectOne({
        url: `${environment.apiServer}/posts`,
        method: 'POST'
      }).flush(expected);
    });
    afterEach(() => {
      mockHttpClient.verify();
    });
  });
  14 行
    TestBed.configureTestingModule({
    imports: [
      HttpClientTestingModule
    ],
    providers: [
      {provide: PostInterfaceToken, useClass: PostService}
    ]
  });
  Angular 有 module 观念,若使用到了其他 module,必须在imports设定;若使用到 DI,则必须在providers设定。
  若只有一个 module,则在AppModule设定。
  但是单元测试时,并没有使用AppModule的设定,因为我们可能在测试时使用其他替代 module,也可能自己实作 fake 另外 DI。
  Angular 提供了TestBed.configureTestingModule(),让我们另外设定跑测试时的imports与providers部分。
  15 行
  imports: [
    HttpClientTestingModule
  ],
  原本HttpClient使用的是HttpClientModule,这会使得HttpClient真的透过网络去打 API,这就不符合单元测试隔离的要求,因此 Angular 另外提供HttpClientTestingModule取代HttpClientModule。
  18 行
  providers: [
    {provide: PostInterfaceToken, useClass: PostService}
  ]
  由于我们要测试的就是PostService,因此PostService?也必须由 DI 帮我们建立。
  但因爲?PostService?是基于PostInterface?建立,因此必须透过PostInterfaceToken?mapping 到?PostService。
  23 行
postService = TestBed.get(PostInterfaceToken, PostService);
  mockHttpClient = TestBed.get(HttpTestingController);
  由providers设定好 interface 与 class 的 mapping 关系后,我们必须透过 DI 建立postService与mockHttpClient。
  其中HttpTestingController相当于 mock 版的?HttpClient,因此取名为mockHttpClient。
  TestBed.get() 其实相当于new,只是这是藉由 DI 帮我们new?而已
  31 行
  it(`should list all posts`, () => {
    /** act */
    const expected: Post[] = [
      {
        id: 1,
        title: 'Design Pattern',
        author: 'Dr. Eric Gamma'
      }
    ];
    postService.listPosts$().subscribe(posts => {
      /** assert */
      expect(posts).toEqual(expected);
    });
    /** arrange */
    const mockResponse: Post[] = [
      {
        id: 1,
        title: 'Design Pattern',
        author: 'Eric Gamma'
      }
    ];
    mockHttpClient.expectOne({
      url: `${environment.apiServer}/posts`,
      method: 'GET'
    }).flush(mockResponse);
  });
  先谈谈如何测试GET。
  3A 原则的?arrange?习惯都会写在最前面,但在 service 的单元测试时,arrange?必须写在最后面,否则会执行错误,稍后会解释原因。
  32 行
  /** act */
  const expected: Post[] = [
    {
      id: 1,
      title: 'Design Pattern',
      author: 'Dr. Eric Gamma'
    }
  ];
  postService.listPosts$().subscribe(posts => {
    /** assert */
    expect(posts).toEqual(expected);
  });
  直接对PostService.listPost$()测试,由于listPost$()?回传?Observable,因此expect()必须写在subscribe()内。
  将预期的测试结果写在expected内。
  一般Observable会在subscribe()后执行,不过在HttpTestingController的设计里,subscribe()会在flush()才执行,稍后会看到flush(),所以此时并还没有执行expect()测试
  46 行
  /** arrange */
  const mockResponse: Post[] = [
    {
      id: 1,
      title: 'Design Pattern',
      author: 'Eric Gamma'
    }
  ];
  mockHttpClient.expectOne({
    url: `${environment.apiServer}/posts`,
    method: 'GET'
  }).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 行
  afterEach(() => {
    mockHttpClient.verify();
  });
  实务上可能真的忘了写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 行
  it(`should add post`, () => {
      /** act */
      const expected: Post = {
        id: 1,
        title: 'OOP',
        author: 'Sam'
      };
      postService.addPost(expected).subscribe(post => {
        /** assert */
        expect(post).toBe(expected);
      });
      /** arrange */
      mockHttpClient.expectOne({
        url: `${environment.apiServer}/posts`,
        method: 'POST'
      }).flush(expected);
    });
  再来谈谈如何测试POST。
  62 行
  /** act */
  const expected: Post = {
    id: 1,
    title: 'OOP',
    author: 'Sam'
  };
  postService.addPost(expected).subscribe(post => {
    /** assert */
    expect(post).toBe(expected);
  });
  直接对?PostService.addPost()?测试,由于addPost()?回传?Observable,因此expect()?必须写在?subscribe()?内。
  将预期的测试结果写在?expected?内。
  74 行
  /** arrange */
  mockHttpClient.expectOne({
    url: `${environment.apiServer}/posts`,
    method: 'POST'
  }).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 单元测试。


上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号