测试(单元或集成)是编程中十分重要的一部分。在当今的软件开发中,单元 / 功能测试已成为软件开发的组成部分。随着 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.json
在 script
局部进行了 test
。
{
...,
"script": {"test": "jest" // or "jasmine"}
}
如果 npm run test
在命令行上运行,则 jest 测试框架将运行 test
文件夹中的所有标准文件,并在命令行上显示后果。
当初,咱们晓得了冀望和构建的内容,咱们持续创立本人的测试框架。咱们的这个框架将基于 Node,也就是说,它将在 Node 上运行测试,稍后将增加对浏览器的反对。
咱们的测试框架将蕴含一个 CLI 局部,该局部将从命令行运行。第二局部将是测试框架的源代码,它将位于 lib 文件夹中,这是框架的外围。
首先,咱们首先创立一个 Node 我的项目。
mkdir kwuo
cd kwuo
npm init -y
装置 chalk 依赖项,咱们将须要它来为测试后果上色:npm i chalk
。
创立一个 lib 文件夹,其中将寄存咱们的文件。
mkdir lib
咱们创立一个 bin 文件夹是因为咱们的框架将用作 Node CLI 工具。
mkdir bin
首先创立 CLI 文件。
在 bin 文件夹中创立 kwuo 文件,并增加以下内容:
#!/usr/bin/env node
process.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/cli
touch 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
在这里,咱们将设置全局变量并实现describe
,it
,expectEach
,beforeEach
,afterAll
,beforeAll
函数。
// lib/index.js
const chalk = require('chalk')
const log = console.log
var beforeEachs = []
var afterEachs = []
var afterAlls = []
var beforeAlls = []
var Totaltests = 0
var passedTests = 0
var failedTests = 0
var 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, total
Tests: ${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 = describe
global.it = it
global.expect = expect
global.afterEach = afterEach
global.beforeEach = beforeEach
global.beforeAll = beforeAll
global.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 预期会是 56
expect(func()).toEqual("nnamdi") // 该函数将返回一个等于“nnamdi”的字符串
expect
函数承受一个要测试的参数,并返回一个蕴含匹配器函数的对象。在这里,它返回一个具备 toBe
和 toEqual
函数的对象,它们具备冀望参数,用于与 expect 函数提供的 value 参数匹配。toBe
应用 ===
将 value 参数与冀望参数匹配,toEqual
应用 ==
测试期望值。如果测试通过或失败,则这些函数将递增 passedTests
和 failedTests
变量,并且还将统计信息记录在 currIt 变量中。
咱们目前只有两个 matcher 函数,还有很多:
- toThrow
- toBeNull
- toBeFalsy
- etc
你能够搜寻它们并实现它们。
接下来,咱们有 it
函数,desc
参数保留测试的形容名称,而 fn
保留函数。它先对 beforeEachs 进行 fun,设置统计,调用 fn
函数,再调用 afterEachs。
describe
函数的作用和 it
一样,但在开始和完结时调用 beforeAlls
和 afterAlls
。
showTestsResults
函数通过 stats
数组进行解析,并在终端上打印通过和失败的测试。
咱们实现了这里的所有函数,并将它们都设置为全局对象,这样才使得测试文件调用它们时不会出错。
回到“lib/cli/cli.js”:
// lib/cli/cli.js
const 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 examples
mkdir examples/math
cd examples/math
npm init -y
创立一个 src 文件夹并增加 add.js 和 sub.js
mkdir src
touch src/add.js src/sub.js
add.js 和 sub.js 将蕴含以下内容:
// src/add.js
function add(a, b) {return a+b}
module.exports = add
// src/sub.js
function sub(a, b) {return a-b}
module.exports = sub
咱们创立一个测试文件夹和测试文件:
mkdir test
touch test/add.spec.js test/sub.spec.js
标准文件将别离测试 add.js 和 sub.js 中的 add 和 sub 函数
// test/sub.spec.js
const 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.js
const 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_PROJECT
npm install kwuo -D
将 package.json 中的“test”更改为此:
{
...
"scripts": {
"test": "kwuo"
...
}
}
总结
咱们建设了咱们的测试框架,在这个过程中,咱们学会了如何应用全局来设置函数和属性在运行时任何中央可见。
咱们看到了如何在我的项目中应用 describe
、it
、expect
和各种匹配函数来运行测试。下一次,你应用 Jest 或 Jasmine,你会更有信念,因为当初你晓得它们是如何工作的。
起源:https://blog.bitsrc.io
作者:Chidume Nnamdi
翻译:公众号《前端全栈开发者》