Angular 深入浅出三十天:表单与测试 Day04 - 开始撰写测试之前必须要知道的二三事

Day4

在开始撰写测试之前,先带大家来了解一下 Angular 预设使用的测试框架 ─ Karma

Karma 的 logo

Karma 的原名是 Testacular , Google 在 2012 年的时候将其开源, 2013 年时将其改名为 Karma ,它是基於 JasmineSelenium 所开发出来的 JavaScript 测试执行过程管理工具(Test Runner)。

一般我们会使用它来撰写单元测试整合测试,测试的档案名称通常会命名为 xxx.spec.ts ,而只要是使用 Angular CLI 所建立的档案,在预设的情况下都会连带产生该档案,像是: xxx.component.spec.tsxxx.service.spec.ts

当我们想要执行测试程序时,只要使用指令 npm test or yarn test or ng test ,就可以看到它的执行结果:

Karma's log

Karma's view

当 Karma 执行起来後,只要我们不停掉它的 server 且不关掉它的视窗,只要我们有修改我们的测试并存档後,它就会侦测到我们的变动并再重新跑一次测试,是个很方便且强大的功能。

关於执行测试时的更多参数,请参考Angular 官方 API 文件

想了解更多的话,可参考网路文章:JavaScript 测试工具之 Karma-Jasmine 的安装和使用详解Karma 官方文件

测试的档案内容

上述提到,在 Angular 里的测试档案一般我们会将其命名为 xxx.spec.ts ,而档案内容大致上会长这样:

Testing Sample

或是这样:

Testing Sample

从中我们可以发现,它是一种巢状式的结构,外层会是一个名字叫 describe 的函式,内层则有许多名为 it 的函式,这些函式各是什麽意思呢?

it

it 指的是 测试案例(Test case),通常会在 describe 函式的里面,使用方式如下所示:

it('说明文字', () => {
  // test content
});

第一个参数是该测试案例的说明文字,让我们在阅读时可以很清楚、直接地知道这个测试案例会有什麽结果,通常建议以 should 做开头,整体阅读起来较为顺畅,例如:

it('should be created', () => {
  // test content
});

或者像是:

it('should have as title "Angular"', () => {
  // test content
});

第二个参数是一个函式,里面就是该测试案例所要执行的程序码,也就是我们实际上要测试的内容。

describe

describe 指的是 测试集合(Test suite),主要是用於将测试案例分组、分类,类似资料夹的概念,这样我们在阅读程序码的时候与其测试结果时,才会比较好阅读

使用方式如下所示:

describe('说明文字', () => {
  // test cases
});

it 一样,第一个参数是该测试集合的说明文字,让我们在阅读时可以很清楚、直接地知道这个测试集合的主要测试目标,例如:

describe('LoginComponent', () => {
  describe('Component logic', () => {
    describe('login', () => {
      // test cases
    });
  });
  describe('Template logic', () => {
    describe('When login button be clicked', () => {
      // test cases
    });
  });
});

第二个参数是一个函式,里面是该测试集合所要执行的测试案例

describe 除了分类、分组的功能外,他还有一个很重要的特性 ─ 作用域(Scoping)

作用域(Scoping)

在写测试案例的时候,我们可能会遇到某些情况是在需要事先做一些配置,又或者是验证完之後需要把某些状态还原,如果将这些事情写在每一个 it 里又觉得很罗嗦且不好维护,这时候我们就会使用以下这些函式来帮我们:

  • beforeAll ─ 在执行所有的测试案例之前,会先执行这里面的程序码。
  • beforeEach ─ 在执行每一个测试案例之前,会先执行这里面的程序码。
  • afterAll ─ 在执行完所有的测试案例之後,会再执行这里面的程序码。
  • afterEach ─ 在执行完每一个测试案例之後,会再执行这里面的程序码。

举个例子,如果我们有个测试集合长这样:

describe('Test Suite', () => {
  beforeAll(() => {
    console.log('beforeAll');
  });

  beforeEach(() => {
    console.log('beforeEach');
  });

  it('test case - 1', () => {
    console.log('test case - 1');
  });

  it('test case - 2', () => {
    console.log('test case - 2');
  });

  afterEach(() => {
    console.log('afterEach');
  });

  afterAll(() => {
    console.log('afterAll');
  });
});

它的执行结果会是这样:

// beforeAll
// beforeEach
// test case - 1
// afterEach
// beforeEach
// test case - 2
// afterEach
// afterAll

从上述结果中可以看出,在一个测试集合里会先执行的是 beforeAll 里的程序,接着会是 beforeEach ,然後才会是测试案例;而在测试案例之後,则会先执行 afterEach 才会轮到下一个测试案例之前的 beforeEach,再接着下一个测试案例,之後一样会是那个测试案例之後的 afterEach 。直到最後没有测试案例时,就执行 afterAll 里面的程序,结束这个测试集合。

有比较理解了吗?如果有的话,我们来试试比较复杂一点的巢状结构:

describe('Test Suite - 1', () => {
  beforeAll(() => {
    console.log('beforeAll - 1');
  });

  beforeEach(() => {
    console.log('beforeEach - 1');
  });

  it('test case - 1', () => {
    console.log('test case - 1');
  });

  it('test case - 2', () => {
    console.log('test case - 2');
  });

  describe('Test Suite - 2', () => {
    beforeAll(() => {
      console.log('beforeAll - 2');
    });

    beforeEach(() => {
      console.log('beforeEach - 2');
    });

    it('test case - 3', () => {
      console.log('test case - 3');
    });

    it('test case - 4', () => {
      console.log('test case - 4');
    });

    afterEach(() => {
      console.log('afterEach - 2');
    });

    afterAll(() => {
      console.log('afterAll - 2');
    });
  });

  afterEach(() => {
    console.log('afterEach - 1');
  });

  afterAll(() => {
    console.log('afterAll - 1');
  });
});

它的执行结果会是这样:

// beforeAll - 1

// beforeEach - 1
// test case - 1
// afterEach - 1

// beforeEach - 1
// test case - 2
// afterEach - 1

// beforeAll - 2
// beforeEach - 1
// beforeEach - 2
// test case - 3
// afterEach - 2
// afterEach - 1

// beforeEach - 1
// beforeEach - 2
// test case - 4
// afterEach - 2
// afterEach - 1
// afterAll - 2

// afterAll - 1

为让大家比较好阅读,我将每个测试案例稍微隔开方便大家观察其中规律。

虽然这个例子比较复杂,但逻辑上来说跟上一个例子一样:在开始测试某测试集合里面的测试案例之前,会先执行该测试集合的 beforeAll ,接着是每一个测试案例的 beforeEach ,然後执行测试案例,执行完测试案例後就是 afterEach

比较特别需要注意的就是当要开始执行 test case - 3 之前,会先执行的是 Test Suite - 2beforeAll 。原因就像上面提过的:「在开始测试某测试集合里面的测试案例之前,会先执行该测试集合的 beforeAll 」, test case - 3Test Suite - 2 里面的测试案例,所以在开始测试 test case - 3 之前,自然会先执行该测试集合里的 beforeAll ,接着是父层测试集合里的 beforeEach ,才会轮到 Test Suite - 2 里面的 beforeEach

这个概念在大多数的前端测试框架里是差不多的,学一次基本适用在大多数的测试框架里, CP 值非常之高。

虽然上述的测试执行过程看似有序,但实际上我们不能依赖这种有序,原因跟如何撰写出优秀的测试有关,不过相信今天的内容应该已经够烧脑了,所以明天再跟大家分享如何撰写出优秀的测试吧!

本日小结

今天的文章内容主要是要让大家在开始撰写测试之前,先对 Angular 的测试框架、测试档案的内容结构有个初步的理解,如此一来有两个好处:

  1. 後续不用再解释,文章内容可以比较精简
  2. 有需要时可以回来复习

此外,今天的重点主要是以下三点:

  1. 认识 Angular 预设所使用的测试框架。
  2. 了解测试档案的内容结构。
  3. 理解作用域(Scoping) 的逻辑。

尤其是关於作用域(Scoping) 的部份,这在後续撰写测试时,会非常常使用,所以如果有任何的问题或是回馈,请务必留言给我让我知道噢!


<<:  [DAY04] 建立 Datastore 和 Dataset (下)

>>:  Day5|【Git】动手建立、初始储存库(Repository)!

Day21:开发自己的 APP 的前置步骤

前言 之後几天会拿来做一个 app~ 在查了一些资料之後, 整理了 开发 APP 的步骤。 开发步骤...

Day 19. Zabbix 上的 script 工具

今天要跟大家介绍的是 script 工具,在 Zabbix Server 安装好预设只会有三个写好的...

[D22] Placeholder

写在前面 Placeholder for test test Placeholder for tes...

「认知」是你观望世界的窗,不时擦拭,光线才能穿透。

「认知」是你观望世界的窗,不时擦拭,光线才能穿透。 Your assumptions are you...

Day 28. slate × Transforms × Node

最後终於来到了我们最後一个章节:『 Transforms 』。 Transform 在 slate...