在Angular中进行单元测试

1,480 阅读11分钟

介绍

单元测试是将应用程序分解为尽可能小的函数,并创建可重复的、自动化的测试用例的过程。如果没有单元测试,不常使用的函数可能长达树叶都不会发现有 bug ,特别是公共的方法,在业务代码中被多个地方调用,当开发者修改了这个公共方法时,无法确保每个调用的地方都能及时同步修改,也可能会因为测试不全面导致项目上线后出现了 bug ,这是非常危险和麻烦的;相反,通过使用单元测试,我们可以在任何代码合并到主分支之前就能验证每个系统函数的功能,可以自动及时的去发现问题,不会等到代码实际应用到产品中时才发现问题。

  • 为什么要进行单元测试?

GitHub的框架、插件如果没有测试用例,是不会被开发者信任的,开发者在选型的时候会非常慎重,甚至不会采用这个,可见测试有多重要。以下主要记录通过 @angular/cli 脚手架生成的项目是如何进行测试以及测试过程的思考和想法?

  • 测试覆盖率到多大才足够?

大部分情况下你可能没有实际或者预算为现有的功能编写100%覆盖率的测试集,但是,即使是一个单一的测试也能够为系统建设贡献价值。因此,在决定从哪里开始写单元测试时,可以从能够获得最大收益的地方开始。一旦有了能提供基本覆盖率的测试集,就可以开始寻找系统中最关键的部分,或者过去频繁出问题的部分,在需求列表中为它们分别创建需求,并确保尽快推动这些需求。比如目前我正在开发的项目公共方法我会加上测试,涉及到金额操作的业务也会加上测试。

  • ”一次只做一件事,并把它做好“,是构建基于单元测试的应用程序的原则。

1. 测试的优点

  • 保证代码的稳定性和可行度,
  • 测试即demo,比如测试一个方法,相当于展示了这个方法是如何使用的。
  • 尽管会多花时间写测试代码,其实会节省很多后续回头追查bug的时间。

2. 测试规则

  • 必须进行测试的是通用、公用的Utils函数。
  • 复杂交互操作需要进行一定的测试。
  • 网络请求可以交给契约测试,或者不进行测试。

3. 测试配置

通过  @angular/cli  创建的项目会为你生成 Jasmine  和 Karma  的配置文件,是可以立即用于测试的,输入以下代码即可:

ng test

或者在 package.json 中配置以下代码,输入 npm run test-coverage ,注意 --watch 设置为 true 可以实时监控测试代码。

 "scripts": {
		"test-coverage": "ng test --code-coverage --watch=true"
 }

karma.conf.js 文件是 karma 配置文件的一部分。 @angular/cli 会基于 angular.json 文件中指定的项目结构和 karma.conf.js 文件,来在内存中构建出完整的运行时配置。

  • karma.conf.js 配置文件
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular-devkit/build-angular/plugins/karma')
    ],
    client: {
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      dir: require('path').join(__dirname, './coverage/fc-angular'),
      reports: ['html', 'lcovonly', 'text-summary'],
      fixWebpackSourcePaths: true
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    restartOnFileChange: true
  });
};

  • test.ts 配置文件
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

  • 用到的测试框架有哪些?

4. jasmine测试框架介绍

新建测试文件

例如我想测试 common.service.ts 这个文件,首先在本文件的当前文件夹内新建 common.service.spec.ts 文件,注意文件名不能写错。新建文件后,开始测试吧!

测试文件的扩展名必须是 .spec.ts,这样工具才能识别出它是一个测试文件,也叫规约(spec)文件。

Jasmine是JavaScript的行为驱动开发测试框架。它不依赖浏览器,DOM或任何JavaScript框架。因此,它适用于网站,Node.js项目或JavaScript可以运行的任何地方。

在Node.js环境中如何配置

package.json 中添加 Jasmine

npm install --save-dev jasmine

在项目中初始化 Jasmine ,输入以下命令:

node node_modules/jasmine/bin/jasmine init

package.json 中设置 jasmine 作为你的 test script

"scripts": { "test": "jasmine" }

输入以下命令运行你的测试代码

npm test

jasmine 常用api

jamine 官方文档API

4.3.1 afterAll(function, timeout)

保证在describe里所有的it执行完成之后调用。

  • Parameters: | Name | Type | Attributes | Default | Description | | :--- | :--- | :--- | :--- | :--- | | function | implementationCallback | | | 包含用于拆除规格的代码的函数。 | | timeout | Int | | jasmine.DEFAULT_TIMEOUT_INTERVAL | 异步afterAll的自定义超时。 |

4.3.2 afterEach(function, timeout)

测试后注入。

  • Parameters: | Name | Type | Attributes | Default | Description | | :--- | :--- | :--- | :--- | :--- | | function | implementationCallback | | | 包含用于拆除规格的代码的函数。 | | timeout | Int | | jasmine.DEFAULT_TIMEOUT_INTERVAL | 自定义超时后异步。 |

4.3.3 beforeAll(function, timeout)

保证在describe里所有的it执行之前调用

  • Parameters: | Name | Type | Attributes | Default | Description | | :--- | :--- | :--- | :--- | :--- | | function | implementationCallback | | | 包含用于设置规格的代码的函数。 | | timeout | Int | | jasmine.DEFAULT_TIMEOUT_INTERVAL | 异步beforeAll的自定义超时。 |

4.3.4 beforeEach(function, timeout)

测试前注入。

  • Parameters: | Name | Type | Attributes | Default | Description | | :--- | :--- | :--- | :--- | :--- | | function | implementationCallback | | | 包含用于设置规格的代码的函数。 | | timeout | Int | | jasmine.DEFAULT_TIMEOUT_INTERVAL | 自定义超时,用于异步beforeEach。 |

4.3.5 describe(description, specDefinitions)

代表一组相似的测试用例,然后具体的测试用例以it开始

  • Parameters: | Name | Type | Description | | :--- | :--- | :--- | | description | String | 小组的文字描述 | | specDefinitions | function | Jasmine调用的函数将定义内部套件和测试用例 |

4.3.6 嵌套describe

不同层次的it执行时,会按从外到内依次执行beforeEach,每个it执行结束时,会按从内到外依次执行            afterEach。

4.3.7 expect(actual)->{matchers}

给测试用例创建一个期望值,检验期望的实际计算值。

  • Parameters: | Name | Type | Description | | :--- | :--- | :--- | | actual | Object | 检验期望的实际计算值。 |

  • Returns:

Type matchers

  • 示例
describe('commonService', () => {
  // 测试getGuid()
  describe('getGuid() function', () => {
    let getGuidTest1 = CommonService.getGuid();
    let getGuidTest2 = CommonService.getGuid();
    let getGuidTest3 = CommonService.getGuid();
    it('should get a guid', () => {
      expect(getGuidTest1.length).toBe(36);
      expect(getGuidTest2.length).toBe(36);
      expect(getGuidTest3.length).toBe(36);
    });
  });
});

4.3.8 expectAsync(actual)->{async-matchers}

创建一个异步的期望值,注意异步期望提供的匹配器将全部返回诺言,这些诺言必须从规范中返回,或者等待使用await以便Jasmine将它们与正确的规范关联。

  • Parameters: | Name | Type | Description | | :--- | :--- | :--- | | actual | Object | 检验期望的实际计算值。 |

  • 示例

await expectAsync(somePromise).toBeResolved();
return expectAsync(somePromise).toBeResolved();

4.3.9 fail(error)

明确的把测试用例标记为失败的。

  • Parameters | Name | Type | Attributes | Description | | :--- | :--- | :--- | :--- | | error | String | Error | | 失败的原因 |

4.3.10 fdescribe(description, specDefinitions)

如果套件或测试用例集中,则仅执行那些套件或测试用例。

  • Parameters:
Name Type Description
description String 小组的文字描述
specDefinitions function Jasmine调用的函数将定义内部套件和规

4.3.11 fit(description, testFunction, timeout)

一个重点如果套件或规格集中,则仅执行那些套件或规格。

  • Parameters: | Name | Type | Attributes | Default | Description | | :--- | :--- | :--- | :--- | :--- | | description | String | | | 此规范正在检查的文字描述。 | | testFunction | implementationCallback | | | 包含测试代码的函数。 | | timeout | Int | | jasmine.DEFAULT_TIMEOUT_INTERVAL | 异步规范的自定义超时。 |

4.3.12 it(description, testFunction, timeout)

定义一个测试用例。 用例应包含一个或多个测试代码状态的期望。期望全部成功的规范将通过,失败的测试将失败。 它的名称是测试目标的代名词,而不是任何事物的缩写。 通过将函数名称和参数说明连接为一个完整的句子,可以使规范更具可读性。

  • Parameters: | Name | Type | Attributes | Default | Description | | :--- | :--- | :--- | :--- | :--- | | description | String | | | 此规范正在检查的文字描述 | | testFunction | implementationCallback | | | 包含测试代码的函数。 如果未提供,则测试即将进行。 | | timeout | Int | | jasmine.DEFAULT_TIMEOUT_INTERVAL | 异步规范的自定义超时。 |

4.3.13 pending(message)

将规格标记为待定,预期结果将被忽略。

  • Parameters: | Name | Type | Attributes | Description | | :--- | :--- | :--- | :--- | | message | String | | 测试待定的原因。 |

4.3.14 spyOn(obj, methodName)

将间接安装到现有对象上。

  • Parameters: | Name | Type | Description | | :--- | :--- | :--- | | obj | Object | 要在其上安装间接的对象。 | | methodName | String | 用间接替代的方法的名称。 |

  • Returns

Type    Spy

4.3.15 spyOnAllFunctions(obj)->{Object}

在对象的所有可写和可配置属性上安装间接程序。

  • Parameters: | Name | Type | Description | | :--- | :--- | :--- | | obj | Object | 在其上安装间接的对象 |

  • Returns

Type 为 Object

4.3.16 spyOnProperty(obj, propertyName, accessType)->{Spy}

在与Object.defineProperty一起安装的属性上将间接安装到现有对象上。

  • Parameters: | Name | Type | Attributes | Default | Description | | :--- | :--- | :--- | :--- | :--- | | obj | Object | | | 在其安装间接的对象 | | propertyName | String | | | 用间接替换的属性的名称。 | | accessType | String | | get | 该属性对Spy的访问类型(get | set) |

  • Returns:

Type Spy

4.3.17 xdescribe(description, specDefinitions)

暂时禁用的描述,xdescribe中的规范将被标记为待定且不会执行。

  • Parameters: | Name | Type | Description | | :--- | :--- | :--- | | description | String | 小组的文字描述 | | specDefinitions | function | Jasmine调用的函数将定义内部套件和和测试用例 |

4.3.18 xit

暂时禁用它,将不会执行。

  • Parameters: | Name | Type | Attributes | Description | | :--- | :--- | :--- | :--- | | description | String | | 此规范正在检查的文字描述。 | | testFunction | implementationCallback | | 包含测试代码的函数将不会执行。 |

4.3.19 toBe(类似于===)

expect(true).toBe(true);

4.3.20 toEqual(比较变量字面量的值)

expect({ foo: 'foo'}).toEqual( {foo: 'foo'} );

4.3.21 toMatch(匹配值与正则表达式)

expect('foo').toMatch(/foo/);

4.3.22 toBeDefined(检验变量是否定义)

var foo = {
    bar: 'foo'
};
expect(foo.bar).toBeDefined();

4.3.23 toBeNull(检验变量是否为null)

var foo = null;
expect(foo).toBeNull();

4.3.24 toBeTruthy(检查变量值是否能转换成布尔型值)

expect({}).toBeTruthy();

4.3.25 toBeFalsy(检查变量值是否能转换成布尔型值)

expect('').toBeFalsy();

4.3.26 toContain(检查在数组中是否包含某个元素)

expect([1,2,4]).toContain(1);

4.3.27 toBeLessThan(检查变量是否小于某个数字)

expect(2).toBeLessThan(10);

4.3.28 toBeGreaterThan(检查变量是否大于某个数字或者变量)

expect(2).toBeGreaterThan(1);

4.3.29 toBeCloseTo(比较两个数在保留几位小数位之后,是否相等,用于数字的精确比较)

expect(3.1).toBeCloseTo(3, 0);

4.3.30 toThrow(检查一个函数是否会throw异常)

expect(function(){ return a + 1;}).toThrow();  // true
expect(function(){ return a + 1;}).not.toThrow(); // false

4.3.31 toHaveBeenCalled(检查一个监听函数是否被调用过)

4.3.32 toHaveBeenCalledWith(检查监听函数调用时的参数匹配信息)

5. 测试代码

5.1 方法测试

比如测试 getGuid() 这个方法,我测试了三次,虽然这三次还是不能保证这个公共方法是百分百正确的,至少比不测试可信多了吧?我现在在思考,一个项目的底层至少得测试多少次呢?

describe('commonService', () => {
  // 测试getGuid()
  describe('getGuid() function', () => {
    let getGuidTest1 = CommonService.getGuid();
    let getGuidTest2 = CommonService.getGuid();
    let getGuidTest3 = CommonService.getGuid();
    it('should get a guid', () => {
      expect(getGuidTest1.length).toBe(36);
      expect(getGuidTest2.length).toBe(36);
      expect(getGuidTest3.length).toBe(36);
    });
  });
});

执行 ng test 控制台会出现以下代码:


Chrome 80.0.3987 (Mac OS X 10.14.6): Executed 1 of 1 SUCCESS (0.014 secs / 0.001 secs)
TOTAL: 1 SUCCESS
TOTAL: 1 SUCCESS

看到 TOTAL: 1 SUCCESS 这两行代码了吗?表示总共测试了1个方法,通过一个。
如果这个方法测试不通过,会出现类似下面的代码,在测试过程中写好描述,测试未通过时,很快就能定位到问题。

Chrome 80.0.3987 (Mac OS X 10.14.6) commonService getGuid() function should get a guid FAILED
        Error: Expected 36 to be 35.
            at <Jasmine>
            at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/webpack:/src/fccore/service/common.service.spec.ts:21:35)
            at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone-evergreen.js:359:1)
            at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone-testing.js:308:1)
Chrome 80.0.3987 (Mac OS X 10.14.6): Executed 1 of 1 (1 FAILED) (0 secs / 0.1 secs)
Chrome 80.0.3987 (Mac OS X 10.14.6) commonService getGuid() function should get a guid FAILED
        Error: Expected 36 to be 35.
            at <Jasmine>
            at UserContext.<anonymous> (http://localhost:9876/_karma_webpack_/webpack:/src/fccore/service/common.service.spec.ts:21:35)
            at ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/dist/zone-evergreen.js:359:1)
            at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpaChrome 80.0.3987 (Mac OS X 10.14.6): Executed 1 of 1 (1 FAILED) ERROR (0.115 secs / 0.1 secs)

接口/契约测试

服务测试

组件测试

建立持续集成环境

持续集成(CI)服务器让你可以配置项目的代码仓库,以便每次提交和收到 Pull Request 时就会运行你的测试。已经有一些像 Circle CI 和 Travis CI 这样的付费 CI 服务器,你还可以使用 Jenkins 或其它软件来搭建你自己的免费 CI 服务器。 虽然 Circle CI 和 Travis CI 是收费服务,但是它们也会为开源项目提供免费服务。 你可以在 GitHub 上创建公开项目,并免费享受这些服务。 当你为 Angular 仓库贡献代码时,就会自动用 Circle CI 和 Travis CI 运行整个测试套件。
如果让我选择,因为公司的代码需要闭源以及节省成本,我会选择免费的 Jenkins 。而 GitHub 的代码是开源的,我会选择 Travis CI 。

性能测试

视觉还原测试

Red Hat测试方法

参考资料

不要害怕寻求别人的帮助,不要害怕分享你的知识,也不要害怕站在台上鼓励他人从事这个领域。最后,不管你做什么,在任何情况下都不要害怕把它们全部写到书上。