测试(单元或集成)是编程中十分重要的一部分。在当今的软件开发中,单元/功能测试已成为软件开发的组成部分。随着Nodejs的呈现,咱们曾经看到了许多超级JS测试框架的公布:Jasmine,Jest等。

单元测试框架

这有时也称为隔离测试,它是测试独立的小段代码的实际。如果你的测试应用某些内部资源(例如网络或数据库),则不是单元测试。

单元测试框架试图以人类可读的格局形容测试,以便非技术人员能够了解所测试的内容。然而,即便你是技术人员,BDD格局的浏览测试也会使你更容易了解所产生的事件。

例如,如果咱们要测试此性能:

function helloWorld() {  return 'Hello world!';}

咱们会像这样写一个jasmine测试标准:

describe('Hello world', () => { ①  it('says hello', () => { ②      expect(helloWorld())③.toEqual('Hello world!'); ④  });});

阐明:

  • describe(string, function) 函数定义了咱们所谓的测试套件,它是各个测试标准的汇合。
  • it(string, function) 函数定义了一个独自的测试标准,其中蕴含一个或多个测试冀望。
  • ③ 预计(理论)表达式就是咱们所说的一个冀望。它与匹配器一起形容应用程序中预期的行为片段。
  • ④ matcher(预期)表达式就是咱们所说的Matcher。如果传入的期望值与传递给Expect函数的理论值不符,则将布尔值与标准进行布尔比拟。

装置和装配

有时候为了测试一个性能,咱们须要进行一些设置,兴许是创立一些测试对象。另外,实现测试后,咱们可能须要执行一些清理流动,兴许咱们须要从硬盘驱动器中删除一些文件。

这些流动称为“设置和装配”(用于清理),Jasmine有一些性能可用来简化此工作:

  • beforeAll 这个函数在describe测试套件中的所有标准运行之前被调用一次。
  • afterAll 在测试套件中的所有标准实现后,该函数将被调用一次。
  • beforeEach 这个函数在每个测试标准之前被调用,it 函数曾经运行。
  • afterEach 在运行每个测试标准之后调用此函数。

在Node中的应用

在Node我的项目中,咱们在与 src 文件夹雷同目录的 test 文件夹中定义单元测试文件:

node_prj    src/        one.js        two.js    test/        one.spec.js        two.spec.js    package.json

该测试蕴含规格文件,这些规格文件是src文件夹中文件的单元测试, package.jsonscript 局部进行了 test

{  ...,  "script": {      "test": "jest" // or "jasmine"    }}

如果 npm run test 在命令行上运行,则jest测试框架将运行 test 文件夹中的所有标准文件,并在命令行上显示后果。

当初,咱们晓得了冀望和构建的内容,咱们持续创立本人的测试框架。咱们的这个框架将基于Node,也就是说,它将在Node上运行测试,稍后将增加对浏览器的反对。

咱们的测试框架将蕴含一个CLI局部,该局部将从命令行运行。第二局部将是测试框架的源代码,它将位于lib文件夹中,这是框架的外围。

首先,咱们首先创立一个Node我的项目。

mkdir kwuocd kwuonpm init -y

装置chalk依赖项,咱们将须要它来为测试后果上色:npm i chalk

创立一个lib文件夹,其中将寄存咱们的文件。

mkdir lib

咱们创立一个bin文件夹是因为咱们的框架将用作Node CLI工具。

mkdir bin

首先创立CLI文件。

在bin文件夹中创立kwuo文件,并增加以下内容:

#!/usr/bin/env nodeprocess.title = 'kwuo'require('../lib/cli/cli')

咱们将hashbang设置为指向 /usr/bin/env node,这样就能够在不应用node命令的状况下运行该文件。

咱们将process的题目设置为“kwuo”,并要求文件“lib/cli/cli”,这样就会调用文件cli.js,从而启动整个测试过程。

当初,咱们创立“lib/cli/cli.js”并填充它。

mkdir lib/clitouch lib/cli/cli.js

该文件将搜寻测试文件夹,在“test”文件夹中获取所有测试文件,而后运行测试文件。

在实现“lib/cli/cli.js”之前,咱们须要设置全局变量。

测试文件中应用了describe,beforeEach,beforeEach,afterAll,beforeAll函数:

describe('Hello world', () => {   it('says hello', () => {     expect(helloWorld()).toEqual('Hello world!');  });});

然而在测试文件中都没有定义。没有ReferenceError的状况下文件和函数如何运行?因为测试框架在运行测试文件之前,会先实现这些函数,并将其设置为globals,所以测试文件调用测试框架曾经设置好的函数不会出错。而且,这使测试框架可能收集测试后果并显示失败或通过的后果。

让咱们在lib文件夹中创立一个 index.js 文件:

touch lib/index.js

在这里,咱们将设置全局变量并实现describeitexpectEachbeforeEachafterAllbeforeAll 函数。

// lib/index.jsconst chalk = require('chalk')const log = console.logvar beforeEachs = []var afterEachs = []var afterAlls = []var beforeAlls = []var Totaltests = 0var passedTests = 0var failedTests = 0var stats = []var currDesc = {  it: []}var currIt = {}function beforeEach(fn) {  beforeEachs.push(fn)}function afterEach(fn) {  afterEachs.push(fn)}function beforeAll(fn) {  beforeAlls.push(fn)}function afterAll(fn) {  afterAlls.push(fn)}function expect(value) {  return {    // Match or Asserts that expected and actual objects are same.    toBe: function(expected) {      if (value === expected) {        currIt.expects.push({ name: `expect ${value} toBe ${expected}`, status: true })        passedTests++      } else {        currIt.expects.push({ name: `expect ${value} toBe ${expected}`, status: false })        failedTests++      }    },    // Match the expected and actual result of the test.    toEqual: function(expected) {      if (value == expected) {        currIt.expects.push({ name: `expect ${value} toEqual ${expected}`, status: true })        passedTests++      } else {        currIt.expects.push({ name: `expect ${value} toEqual ${expected}`, status: false })        failedTests++      }    }  }}function it(desc, fn) {  Totaltests++  if (beforeEachs) {    for (var index = 0; index < beforeEachs.length; index++) {      beforeEachs[index].apply(this)    }  }  //var f = stats[stats.length - 1]  currIt = {    name: desc,    expects: []  }  //f.push(desc)  fn.apply(this)  for (var index = 0; index < afterEachs.length; index++) {    afterEachs[index].apply(this)  }  currDesc.it.push(currIt)}function describe(desc, fn) {  currDesc = {    it: []  }  for (var index = 0; index < beforeAlls.length; index++) {    beforeAlls[index].apply(this)  }  currDesc.name = desc  fn.apply(this)  for (var index = 0; index < afterAlls.length; index++) {    afterAlls[index].apply(this)  }  stats.push(currDesc)}exports.showTestsResults = function showTestsResults() {    console.log(`Total Test: ${Totaltests}    Test Suites: passed, totalTests: ${passedTests} passed, ${Totaltests} total`)  const logTitle = failedTests > 0 ? chalk.bgRed : chalk.bgGreen  log(logTitle('Test Suites'))  for (var index = 0; index < stats.length; index++) {    var e = stats[index];    const descName = e.name    const its = e.it    log(descName)    for (var i = 0; i < its.length; i++) {      var _e = its[i];      log(`   ${_e.name}`)      for (var ii = 0; ii < _e.expects.length; ii++) {        const expect = _e.expects[ii]        log(`      ${expect.status === true ? chalk.green('√') : chalk.red('X') } ${expect.name}`)      }    }    log()  }}global.describe = describeglobal.it = itglobal.expect = expectglobal.afterEach = afterEachglobal.beforeEach = beforeEachglobal.beforeAll = beforeAllglobal.afterAll = afterAll

在开始的时候,咱们须要应用chalk库,因为咱们要用它来把失败的测试写成红色,把通过的测试写成绿色。咱们将 console.log 缩短为 log。

接下来,咱们设置beforeEachs,afterEachs,afterAlls,beforeAlls的数组。beforeEachs将保留在它所附加的 it 函数开始时调用的函数;afterEachs将在它所附加的 it 函数的开端调用;beforeEachs和afterEachs别离在 describe 函数的开始和结尾处调用。

咱们设置了 Totaltests 来保留运行的测试数量, passTests 保留已通过的测试数, failedTests 保留失败的测试数。

stats 收集每个describe函数的stats,curDesc 指定以后运行的describe函数来帮忙收集测试数据,currIt 保留以后正在执行的 it 函数,以帮忙收集测试数据。

咱们设置了beforeEach、afterEach、beforeAll和afterAll函数,它们将函数参数推入相应的数组,afterAll推入afterAlls数组,beforeEach推入beforeEachs数组,等等。

接下来是expect函数,此函数进行测试:

expect(56).toBe(56) // 通过测试56预期会是56expect(func()).toEqual("nnamdi") // 该函数将返回一个等于“nnamdi”的字符串

expect 函数承受一个要测试的参数,并返回一个蕴含匹配器函数的对象。在这里,它返回一个具备 toBetoEqual 函数的对象,它们具备冀望参数,用于与expect函数提供的value参数匹配。toBe 应用 === 将value参数与冀望参数匹配,toEqual 应用 == 测试期望值。如果测试通过或失败,则这些函数将递增 passedTestsfailedTests 变量,并且还将统计信息记录在currIt变量中。

咱们目前只有两个matcher函数,还有很多:

  • toThrow
  • toBeNull
  • toBeFalsy
  • etc

你能够搜寻它们并实现它们。

接下来,咱们有 it 函数,desc 参数保留测试的形容名称,而 fn 保留函数。它先对beforeEachs进行fun,设置统计,调用 fn 函数,再调用afterEachs。

describe 函数的作用和 it 一样,但在开始和完结时调用 beforeAllsafterAlls

showTestsResults 函数通过 stats 数组进行解析,并在终端上打印通过和失败的测试。

咱们实现了这里的所有函数,并将它们都设置为全局对象,这样才使得测试文件调用它们时不会出错。

回到“lib/cli/cli.js”:

// lib/cli/cli.jsconst path = require('path')const fs = require('fs')const { showTestsResults } = require('./../')

首先,它从“lib/index”导入函数 showTestsResult,该函数将在终端显示运行测试文件的后果。另外,导入此文件将设置全局变量。

让咱们持续:

run 函数是这里的次要函数,这里调用它,能够疏导整个过程。它搜寻 test 文件夹 searchTestFolder,而后在数组getTestFiles 中获取测试文件,它循环遍历测试文件数组并运行它们 runTestFiles

  • searchTestFolder:应用 fs#existSync 办法查看我的项目中是否存在“test/”文件夹。
  • getTestFiles:此函数应用 fs#readdirSync 办法读取“test”文件夹的内容并返回它们。
  • runTestFiles:它承受数组中的文件,应用 forEach 办法循环遍历它们,并应用 require 办法运行每个文件。

kwuo文件夹构造如下所示:

测试咱们的框架

咱们曾经实现了咱们的测试框架,让咱们通过一个实在的Node我的项目对其进行测试。

咱们创立一个Node我的项目:

mkdir examplesmkdir examples/mathcd examples/mathnpm init -y

创立一个src文件夹并增加add.js和sub.js

mkdir srctouch src/add.js src/sub.js

add.js和sub.js将蕴含以下内容:

// src/add.jsfunction add(a, b) {    return a+b}module.exports = add// src/sub.jsfunction sub(a, b) {    return a-b}module.exports = sub

咱们创立一个测试文件夹和测试文件:

mkdir testtouch test/add.spec.js test/sub.spec.js

标准文件将别离测试add.js和sub.js中的add和sub函数

// test/sub.spec.jsconst sub = require('../src/sub')describe("Subtract numbers", () => {  it("should subtract 1 from 2", () => {    expect(sub(2, 1)).toEqual(1)  })    it("should subtract 2 from 3", () => {    expect(sub(3, 2)).toEqual(1)  })})// test/add.spec.jsconst add = require('../src/add')describe("Add numbers", () => {  it("should add 1 to 2", () => {    expect(add(1, 2)).toEqual(3)  })    it("should add 2 to 3", () => {    expect(add(2, 3)).toEqual(5)  })})describe('Concat Strings', () => {  let expected;  beforeEach(() => {    expected = "Hello";  });    afterEach(() => {    expected = "";  });    it('add Hello + World', () => {    expect(add("Hello", "World"))      .toEqual(expected);  });});

当初,咱们将在package.json的“script”局部中运行“test”以运行咱们的测试框架:

{  "name": "math",  "version": "1.0.0",  "description": "",  "main": "index.js",  "scripts": {    "test": "kwuo"  },  "keywords": [],  "author": "Chidume Nnamdi <kurtwanger40@gmail.com>",  "license": "ISC"}

咱们在命令行上运行 npm run test,后果将是这样的:

看,它给咱们展现了统计数据,通过测试的总数,以及带有“失败”或“通过”标记的测试套件列表。看到通过的测试冀望“add Hello + World”,它将返回“HelloWorld”,但咱们冀望返回“Hello”。如果咱们纠正它并从新运行测试,所有测试都将通过。

// test/add.spec.js...describe('Concat Strings', () => {  let expected;  beforeEach(() => {    expected = "Hello";  });    afterEach(() => {    expected = "";  });    it('add Hello + World', () => {    expect(add("Hello", ""))      .toEqual(expected);  });});

看,咱们的测试框架像Jest和Jasmine一样工作。它仅在Node上运行,在下一篇文章中,咱们将使其在浏览器上运行。

代码在Github上

Github仓库地址:philipszdavido/kwuoKwuo

你能够应用来自NPM的框架:

cd IN_YOUR_NODE_PROJECTnpm install kwuo -D

将package.json中的“test”更改为此:

{  ...  "scripts": {    "test": "kwuo"    ...  }}

总结

咱们建设了咱们的测试框架,在这个过程中,咱们学会了如何应用全局来设置函数和属性在运行时任何中央可见。

咱们看到了如何在我的项目中应用 describeitexpect 和各种匹配函数来运行测试。下一次,你应用Jest或Jasmine,你会更有信念,因为当初你晓得它们是如何工作的。


起源:https://blog.bitsrc.io
作者:Chidume Nnamdi
翻译:公众号《前端全栈开发者》