51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 1862|回复: 1
打印 上一主题 下一主题

[讨论] 如何对 Service 单元测试 ?

[复制链接]
  • TA的每日心情
    郁闷
    2022-8-29 14:43
  • 签到天数: 1 天

    连续签到: 1 天

    [LV.1]测试小兵

    跳转到指定楼层
    1#
    发表于 2018-1-30 11:52:59 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    如何对 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?而已



    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

  • TA的每日心情
    郁闷
    2022-8-29 14:43
  • 签到天数: 1 天

    连续签到: 1 天

    [LV.1]测试小兵

    2#
     楼主| 发表于 2018-1-30 11:57:46 | 只看该作者
    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 单元测试。




    回复 支持 反对

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-27 14:34 , Processed in 0.061149 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表