Angular:Jasmine + Karma 测试实战

Angular 提供了Jasmine + Karma 的单元测试,还不了解的同学请看Angular单元测试浅说
Angular中需要为每个被测试的文件创建以 .spec.ts 结尾的文件作为测试文件,除了引入测试文件本身需要的依赖外,还需要将被测试文件所属的依赖添加到测试文件。
在这里发现了 vscode 插件: shark-extension(yangbo),可以一键生成测试文件。

使用shark-extension插件生成测试文件

右键点击被测试的文件(以Component为例),选择 generate unit test,就会自动生成 .spec.ts文件


生成测试文件

post.component.spec.ts

生成的测试文件会为每个function生成一个用例:


post.component.spec.ts

并且会自动添加所需要的依赖,上部分是测试需要的依赖,下部分为Component的依赖:

import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';

import { PostComponent } from './post.component';
import { Component,Directive } from '@angular/core';
import { Router,NzMessageService,PostService,UserService,RegularService,EmitService } from 'date-fns/difference_in_days';

大家会看到最后一条依赖引入错误,查看了Component发现是因为引入了

import * as differenceInDays from 'date-fns/difference_in_days';

这个时候就需要手动修改:

import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';

import { PostComponent } from './post.component';
import { Component,Directive } from '@angular/core';
import { Router } from '@angular/router';
import { NzMessageService, NzNotificationService } from 'ng-zorro-antd';
import { PostService } from '../../common/services/post.service';
import { UserService } from '../../common/services/user.service';
import { RegularService } from '../../common/services/regular.service';
import { EmitService } from '../../common/services/emit.service';
import * as differenceInDays from 'date-fns/difference_in_days';

依赖解决了后,执行测试,看看是否能将测试跑起来(这个时候是空测试,仅仅是为了检查环境是否正确,依赖是否全部引入)

ng-test 

发现报错了:


error1.png

error2.png

可以看到都是StaticInjectorError,发现是公用的模块没有引用(也有可能是Pipe错误),将SharedModule和DelonModule引入:

...
import { SharedModule } from '@shared/shared.module';
import { DelonModule } from '../../delon.module';

...
imports: [
        ...
        SharedModule,
        DelonModule
],

执行ng-test ,发现测试通过了。


测试通过

Jasmine常用 Matchers 和 Setup and Teardown

Matchers是断言匹配操作,在实际值与期望值之间进行比较,并将结果通知Jasmine,最终Jasmine会判断此 Spec 成功还是失败。
Setup 与 Teardown相当于测试之前的准备工作,我们可以将重复的 Setup 与 Teardown 代码,放在与之相对应的 beforeEach 与 afterEach 全局函数里面。
了解常用的Matchers和Setup and Teardown有助于更快捷的编写测试代码。

Matchers

测试时会根据expect的实际传入的值和期望值进行比较,返回true,表示成功;如果为false,则表示失败。下列是经常用到的matchers:
查看更多信息点击这里

expect(array).toContain(member);
expect(fn).toThrow(string);
expect(fn).toThrowError(string);
expect(instance).toBe(instance);
expect(mixed).toBeDefined();
expect(mixed).toBeFalsy();
expect(mixed).toBeNull();
expect(mixed).toBeTruthy();
expect(mixed).toBeUndefined();
expect(mixed).toEqual(mixed);
expect(mixed).toMatch(pattern);
expect(number).toBeCloseTo(number, decimalPlaces);
expect(number).toBeGreaterThan(number);
expect(number).toBeLessThan(number);
expect(number).toBeNaN();
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(number);
expect(spy).toHaveBeenCalledWith(...arguments);

Setup and Teardown

测试有一些功能时需要一些额外的设置,测试完成后又需要删除,就需要用到下列function

  • beforeAll
    在执行所有测试之前调用一次(describe function之前)
  • afterAll
    在执行所有测试之后调用
  • beforeEach
    在执行每个测试之前调用(it function之前)
  • afterEach
    在执行每个测试之后调用

测试Component

  • 数据绑定
  • 组件的inputs和outputs

准备工作

在写测试逻辑之前,需要做一些准备工作。

  1. 声明页面元素:DebugElement
    DebugElement是Angular的抽象层,可以安全的横跨其支持的所有平台。Angular 不再创建 HTML 元素树,而是创建 DebugElement树,其中包裹着相应运行平台上的原生元素。
    下列元素后面会在beforeEach中获取为页面的input或者button等。
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
...
let submitEl: DebugElement;
let loginEl: DebugElement;
let passwordEl: DebugElement;
let h1: HTMLElement
  1. 查找元素:By.css()
import  { By }  from  '@angular/platform-browser';
...
// beforeEach中
submitEl = fixture.debugElement.query(By.css('button'));

// 解包
submitEl.nativeElement

注意:

  • By.css() 静态方法使用标准 CSS 选择器选择了一些 DebugElement 节点。

  • 这次查询返回了 <button> 元素的一个 DebugElement。

  • 必须解包此结果,以获取这个 <button> 元素。

beforeEach整体如下:

beforeEach(() => {
    fixture = TestBed.createComponent(UserLoginComponent);
    component = fixture.debugElement.componentInstance;
    submitEl = fixture.debugElement.query(By.css('button'));
    loginEl = fixture.debugElement.query(By.css('input[type=username]'));
    passwordEl = fixture.debugElement.query(By.css('input[type=password]'));
    h1 = fixture.nativeElement.querySelector('h1');
  });

测试数据绑定

测试页面 title 是否会绑定到页面:
因为绑定是在 Angular 执行变更检测时才发生的,所以需要通过调用 fixture.detectChanges() 来要求 TestBed 执行数据绑定。

it('数据绑定', () => {
    fixture.detectChanges();
    expect(h1.textContent).toContain(component.title);
  });

组件的inputs和outputs

it('将按钮enabled设置为false', () => {
    component.enabled = false;
    fixture.detectChanges();
    expect(submitEl.nativeElement.disabled).toBeTruthy();
});

it('输入用户名密码,点击登录', () => {
    let username = '';
    let pwd = '';

    loginEl.nativeElement.value = "17711111111";
    passwordEl.nativeElement.value = "123456";

    component.loggedIn.subscribe(value => {
        username = value.username;
        pwd = value.pwd;
        expect(username).toBe("17711111111");
        expect(pwd).toBe("123456");
     });
    submitEl.triggerEventHandler('click', null);
});

如果想让组件自动检测更新,使用 ComponentFixtureAutoDetect ,配置 TestBed:

import  { ComponentFixtureAutoDetect }  from  '@angular/core/testing';
...
providers:  [  
...
{ provide:  ComponentFixtureAutoDetect,  useValue:  true  }  ]

测试Service

  • 模拟后端返回数据

准备工作

  1. 导入HttpClientTestingModule和HttpTestingController
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
...
let httpTestingController: HttpTestingController;
...

imports: [
...
HttpClientTestingModule]
  1. 获取httpTestingController
beforeEach(() => {
    ...
    httpTestingController = TestBed.get(HttpTestingController);
});

3.在afterEach中调用verify,确保没有未完成的请求

afterEach(() => {
    httpTestingController.verify();
 });

测试http返回List

首先mock一个数组当作后端返回的数据,可以判断数组长度,数据字段等。
如果HttpEventType的类型为Response,则表明响应事件的返回等于模拟HTTP请求的数据。
主要代码:

import { NO_ERRORS_SCHEMA } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { HttpEvent, HttpEventType } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { _HttpClient } from '@delon/theme';
import { URL } from '../url';
describe('UserService', () => {
  let usersService: UserService;
  let httpTestingController: HttpTestingController;
  beforeEach(() => TestBed.configureTestingModule({
    imports: [HttpClientTestingModule],
    providers: [
      UserService,
      _HttpClient
    ],
    schemas: [NO_ERRORS_SCHEMA]
  }));

  afterEach(() => {
    httpTestingController.verify();
  });

  beforeEach(() => {
    usersService = TestBed.get(UserService);
    httpTestingController = TestBed.get(HttpTestingController);
  });

it('should run #getUserList()', () => {
      const mockUsers = [
        { id: 1, username: 'user1'},
        { id: 1, username: 'user2'},
      ];

      usersService.getUserList({}).subscribe((event: HttpEvent<any>) => {
        switch (event.type) {
          case HttpEventType.Response:
            expect(event.body).toEqual(mockUsers);
        }
      });

      const mockReq = httpTestingController.expectOne(URL.USER);
      expect(mockReq.cancelled).toBeFalsy();
      expect(mockReq.request.responseType).toEqual('json');
      mockReq.flush(mockUsers);
  });

  afterEach(() => {
    TestBed.resetTestingModule();
  });
});

测试Directive和Pipe

测试指令:
需要获取元素,调用 triggerEventHandler 改变元素属性:

triggerEventHandler 为 Angular DebugElement实例提供的一种方触发事件。

it('鼠标移动改变颜色', () => {
    inputEl.triggerEventHandler('mouseover', null);
    fixture.detectChanges();
    expect(inputEl.nativeElement.style.backgroundColor).toBe('blue');

    inputEl.triggerEventHandler('mouseout', null);
    fixture.detectChanges();
    expect(inputEl.nativeElement.style.backgroundColor).toBe('inherit');
  });

测试管道:
需要获取元素,调用 transform 判断返回值:

it('数值除以100', () => {
    const result = pipeInstance.transform(300);
    expect(result).toBe(3);
 });

代码覆盖率报告

在 angular.json 中添加可生成测试覆盖率报告:

"test":{ 
  "options":{   
  "codeCoverage": true 
  }
}

然后执行,会在根目录下生成 coverage 文件夹:

ng test --code-coverage
测试覆盖率报告

复制 index.html 目录在浏览器中打开,就可以看到测试报告了:


index.html
image.png

以上就是在Angular测试中经常需要用到的干货,在实际应用中可能需要组合起来测试,想知道更详细的内容,可查看Angular官网测试部分。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容