通过构建自己的JavaScript测试框架来了解JS测试

新闻 前端
在当今的软件开发中,单元/功能测试已成为软件开发的组成部分。随着Nodejs的出现,我们已经看到了许多超级JS测试框架的发布:Jasmine,Jest等。

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

单元测试框架

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

单元测试框架试图以人类可读的格式描述测试,以便非技术人员可以理解所测试的内容。然而,即使你是技术人员,BDD格式的阅读测试也会使你更容易理解所发生的事情。

例如,如果我们要测试此功能:

  1. function helloWorld() { 
  2.   return 'Hello world!'

我们会像这样写一个jasmine测试规范:

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

说明:

  1. describe(string, function) 
  2. it(string, function) 

安装和拆卸

有时候为了测试一个功能,我们需要进行一些设置,也许是创建一些测试对象。另外,完成测试后,我们可能需要执行一些清理活动,也许我们需要从硬盘驱动器中删除一些文件。

这些活动称为“设置和拆卸”(用于清理),Jasmine有一些功能可用来简化此工作:

  • beforeAll 这个函数在describe测试套件中的所有规范运行之前被调用一次。
  • afterAll 在测试套件中的所有规范完成后,该函数将被调用一次。
  • beforeEach 这个函数在每个测试规范之前被调用, it 函数已经运行。
  • afterEach 在运行每个测试规范之后调用此函数。

在Node中的使用

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

  1. node_prj 
  2.     src/ 
  3.         one.js 
  4.         two.js 
  5.     test/ 
  6.         one.spec.js 
  7.         two.spec.js 
  8.     package.json 

该测试包含规格文件,这些规格文件是src文件夹中文件的单元测试, package.json 在 script 部分进行了 test 。

  1.   ..., 
  2.   "script": { 
  3.       "test""jest" // or "jasmine" 
  4.     } 

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

现在,我们知道了期望和构建的内容,我们继续创建自己的测试框架。我们的这个框架将基于Node,也就是说,它将在Node上运行测试,稍后将添加对浏览器的支持。

我们的测试框架将包含一个CLI部分,该部分将从命令行运行。第二部分将是测试框架的源代码,它将位于lib文件夹中,这是框架的核心。

首先,我们首先创建一个Node项目。

  1. mkdir kwuo 
  2. cd kwuo 
  3. npm init -y 

安装chalk依赖项,我们将需要它来为测试结果上色: npm i chalk 。

创建一个lib文件夹,其中将存放我们的文件。

  1. mkdir lib 

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

  1. mkdir bin 

首先创建CLI文件。

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

  1. #!/usr/bin/env node 
  2.  
  3. process.title = 'kwuo' 
  4. require('../lib/cli/cli'

我们将hashbang设置为指向 /usr/bin/env node,这样就可以在不使用node命令的情况下运行该文件。

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

现在,我们创建“lib/cli/cli.js”并填充它。

  1. mkdir lib/cli 
  2. touch lib/cli/cli.js 

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

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

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

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

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

让我们在lib文件夹中创建一个 index.js 文件:

  1. touch lib/index.js 

在这里,我们将设置全局变量并实现 describe , it , expectEach , beforeEach , afterAll , beforeAll 函数。

  1. // lib/index.js 
  2.  
  3. const chalk = require('chalk'
  4. const log = console.log 
  5. var beforeEachs = [] 
  6. var afterEachs = [] 
  7. var afterAlls = [] 
  8. var beforeAlls = [] 
  9. var Totaltests = 0 
  10. var passedTests = 0 
  11. var failedTests = 0 
  12. var stats = [] 
  13. var currDesc = { 
  14.   it: [] 
  15.  
  16. var currIt = {} 
  17.  
  18. function beforeEach(fn) { 
  19.   beforeEachs.push(fn) 
  20.  
  21. function afterEach(fn) { 
  22.   afterEachs.push(fn) 
  23.  
  24. function beforeAll(fn) { 
  25.   beforeAlls.push(fn) 
  26.  
  27. function afterAll(fn) { 
  28.   afterAlls.push(fn) 
  29.  
  30. function expect(value) { 
  31.   return { 
  32.  
  33.     // Match or Asserts that expected and actual objects are same. 
  34.     toBe: function(expected) { 
  35.       if (value === expected) { 
  36.         currIt.expects.push({ name: `expect ${value} toBe ${expected}`, status: true }) 
  37.         passedTests++ 
  38.       } else { 
  39.         currIt.expects.push({ name: `expect ${value} toBe ${expected}`, status: false }) 
  40.         failedTests++ 
  41.       } 
  42.     }, 
  43.  
  44.     // Match the expected and actual result of the test. 
  45.     toEqual: function(expected) { 
  46.       if (value == expected) { 
  47.         currIt.expects.push({ name: `expect ${value} toEqual ${expected}`, status: true }) 
  48.         passedTests++ 
  49.       } else { 
  50.         currIt.expects.push({ name: `expect ${value} toEqual ${expected}`, status: false }) 
  51.         failedTests++ 
  52.       } 
  53.     } 
  54.   } 
  55.  
  56. function it(desc, fn) { 
  57.   Totaltests++ 
  58.   if (beforeEachs) { 
  59.     for (var index = 0; index < beforeEachs.length; index++) { 
  60.       beforeEachs[index].apply(this
  61.     } 
  62.   } 
  63.   //var f = stats[stats.length - 1] 
  64.   currIt = { 
  65.     name: desc, 
  66.     expects: [] 
  67.   } 
  68.   //f.push(desc) 
  69.   fn.apply(this
  70.   for (var index = 0; index < afterEachs.length; index++) { 
  71.     afterEachs[index].apply(this
  72.   } 
  73.   currDesc.it.push(currIt) 
  74.  
  75. function describe(desc, fn) { 
  76.   currDesc = { 
  77.     it: [] 
  78.   } 
  79.   for (var index = 0; index < beforeAlls.length; index++) { 
  80.     beforeAlls[index].apply(this
  81.   } 
  82.   currDesc.name = desc 
  83.   fn.apply(this
  84.   for (var index = 0; index < afterAlls.length; index++) { 
  85.     afterAlls[index].apply(this
  86.   } 
  87.   stats.push(currDesc) 
  88.  
  89. exports.showTestsResults = function showTestsResults() { 
  90.     console.log(`Total Test: ${Totaltests}     
  91. Test Suites: passed, total 
  92. Tests: ${passedTests} passed, ${Totaltests} total 
  93. `) 
  94.   const logTitle = failedTests > 0 ? chalk.bgRed : chalk.bgGreen 
  95.   log(logTitle('Test Suites')) 
  96.   for (var index = 0; index < stats.length; index++) { 
  97.     var e = stats[index]; 
  98.     const descName = e.name 
  99.     const its = e.it 
  100.     log(descName) 
  101.     for (var i = 0; i < its.length; i++) { 
  102.       var _e = its[i]; 
  103.       log(`   ${_e.name}`) 
  104.       for (var ii = 0; ii < _e.expects.length; ii++) { 
  105.         const expect = _e.expects[ii] 
  106.         log(`      ${expect.status === true ? chalk.green('√') : chalk.red('X') } ${expect.name}`) 
  107.       } 
  108.     } 
  109.     log() 
  110.   } 
  111.  
  112. global.describe = describe 
  113. global.it = it 
  114. global.expect = expect 
  115. global.afterEach = afterEach 
  116. global.beforeEach = beforeEach 
  117. global.beforeAll = beforeAll 
  118. 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函数,此函数进行测试:

  1. expect(56).toBe(56// 经过测试56预期会是56 
  2. 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”:

  1. // lib/cli/cli.js 
  2. const path = require('path'
  3. const fs = require('fs'
  4. 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项目:

  1. mkdir examples 
  2. mkdir examples/math 
  3. cd examples/math 
  4. npm init -y 

创建一个src文件夹并添加add.js和sub.js

  1. mkdir src 
  2. touch src/add.js src/sub.js 

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

  1. // src/add.js 
  2. function add(a, b) { 
  3.     return a+b 
  4.  
  5. module.exports = add 
  6.  
  7. // src/sub.js 
  8. function sub(a, b) { 
  9.     return a-b 
  10.  
  11. module.exports = sub 

我们创建一个测试文件夹和测试文件:

  1. mkdir test 
  2. touch test/add.spec.js test/sub.spec.js 

规范文件将分别测试add.js和sub.js中的add和sub函数

  1. // test/sub.spec.js 
  2. const sub = require('../src/sub'
  3. describe("Subtract numbers", () => { 
  4.   it("should subtract 1 from 2", () => { 
  5.     expect(sub(21)).toEqual(1
  6.   }) 
  7.    
  8.   it("should subtract 2 from 3", () => { 
  9.     expect(sub(32)).toEqual(1
  10.   }) 
  11. }) 
  12.  
  13. // test/add.spec.js 
  14. const add = require('../src/add'
  15. describe("Add numbers", () => { 
  16.   it("should add 1 to 2", () => { 
  17.     expect(add(12)).toEqual(3
  18.   }) 
  19.    
  20.   it("should add 2 to 3", () => { 
  21.     expect(add(23)).toEqual(5
  22.   }) 
  23. }) 
  24. describe('Concat Strings', () => { 
  25.   let expected; 
  26.   beforeEach(() => { 
  27.     expected = "Hello"
  28.   }); 
  29.    
  30.   afterEach(() => { 
  31.     expected = ""
  32.   }); 
  33.    
  34.   it('add Hello + World', () => { 
  35.     expect(add("Hello""World")) 
  36.       .toEqual(expected); 
  37.   }); 
  38. }); 

现在,我们将在package.json的“script”部分中运行“test”以运行我们的测试框架:

  1.   "name""math"
  2.   "version""1.0.0"
  3.   "description"""
  4.   "main""index.js"
  5.   "scripts": { 
  6.     "test""kwuo" 
  7.   }, 
  8.   "keywords": [], 
  9.   "author""Chidume Nnamdi <kurtwanger40@gmail.com>"
  10.   "license""ISC" 

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

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

  1. // test/add.spec.js 
  2. ... 
  3. describe('Concat Strings', () => { 
  4.   let expected; 
  5.   beforeEach(() => { 
  6.     expected = "Hello"
  7.   }); 
  8.    
  9.   afterEach(() => { 
  10.     expected = ""
  11.   }); 
  12.    
  13.   it('add Hello + World', () => { 
  14.     expect(add("Hello""")) 
  15.       .toEqual(expected); 
  16.   }); 
  17. }); 

 

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

代码在Github上

Github仓库地址: philipszdavido/kwuoKwuo

你可以使用来自NPM的框架:

  1. cd IN_YOUR_NODE_PROJECT 
  2. npm install kwuo -D 

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

  1.   ... 
  2.   "scripts": { 
  3.     "test""kwuo" 
  4.     ... 
  5.   } 

总结

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

我们看到了如何在项目中使用 describe 、 it 、 expect 和各种匹配函数来运行测试。下一次,你使用Jest或Jasmine,你会更有信心,因为现在你知道它们是如何工作的。

 

 

责任编辑:张燕妮 来源: segmentfault
相关推荐

2022-01-06 22:04:03

JavaScript语言开发

2023-01-31 16:35:34

JavaScript测试框架

2012-12-18 13:32:45

IBMdW

2023-09-13 11:40:12

2011-06-03 17:06:09

自动化测试

2009-07-22 14:49:18

ibmdwPython测试

2022-05-24 07:51:05

测试模型测试单元测试

2023-11-08 13:18:00

JestJavaScript框架

2017-08-10 14:04:25

前端JavaScript函数性能

2020-06-24 07:44:45

JavaScript开发代码

2020-01-10 15:57:03

JavaScript开发 技巧

2020-01-10 10:48:27

JavaScript框架StateOfJS

2011-10-08 13:45:12

JavaScript

2015-09-06 10:58:36

PHP框架搭建结构

2017-05-16 16:28:21

互联网

2021-08-17 09:00:00

架构PythonWeb

2011-03-30 16:54:13

JUnit

2009-06-22 15:52:15

JSF测试框架

2022-05-12 09:37:03

测试JUnit开发

2016-11-30 18:35:03

JavaScript
点赞
收藏

51CTO技术栈公众号