如何在Node.js中使用Mocha和Chai进行测试

327 阅读7分钟

在Node.js中使用Mocha和Chai进行测试

在这篇文章中,我们将向你介绍测试软件的第二个层次,集成测试,以及常见的测试实践,如嘲弄和存根。

首先,让我们了解一下什么是集成测试以及为什么我们需要它。


什么是集成测试?

集成测试结合了几个单元(我们在单元测试中测试过的),并将它们作为一个单独的组进行测试。它检查系统中与单元之间的交互有关的错误。

我们可以使用集成测试来测试从数据库、远程API和API端点的数据检索。集成测试的使用增加了测试所覆盖的代码的范围。

通常情况下,集成测试是开发人员的责任。但独立的测试人员进行测试的情况并不罕见。


用Mocha和Chai在Node.js中进行集成测试

为了开始进行Node.js集成测试,我们将使用Mocha和Chai NPM包。我们将使用一个带有REST端点的Express服务器作为我们的测试目的。为了在测试期间向该服务器发送HTTP请求,我们使用一个新的包,名为chai-http 。在继续之前,请确保使用npm install命令安装chai-http


测试异步函数

当使用Mocha测试异步函数时,我们必须遵循与之前教程中使用的格式略有不同。尽管我用了 "新 "这个词来描述这种格式,但对于Node.js开发者来说,这并不新鲜。我们可以传递一个回调,或者如果异步函数返回一个Promise,在 "it "函数内部使用Promises或async/await来处理异步代码,就像我们在Node.js中通常做的那样。

describe("Test asynchronous code", () => {

	//using a callback function
	it("should return the intended output", done => {
    	callToAsyncFunction(input, (result) = {
        	//implement testing logic

        	//call the callback function
        	done()
    	})
	})

	//using promises
	it("should return another output", () => {
    	return callToAsyncFunction(input).then(result => {
        	//implement result testing logic
    	})
	})

	//using async/await
	it("should return a different output", async () => {
    	let result = await callToAsyncFunction(input)
    	//implement testing logic

	})
})

编写测试案例来测试你的API

对于你的第一个集成测试的实现,我们正在使用一个REST API,在请求时从数据库中检索相关数据,并将它们发回给客户端。假设数据库(我们使用的是MongoDB数据库)和应用程序的数据模型已经设置好了,以明确测试的实施。

POSTroute /dogs 用于使用从客户端通过POST请求发送的数据将一只新的狗保存到数据库中。我们在这一步中使用的狗模型有3个属性:狗的名字、年龄和品种。我们需要将这些数据与HTTP请求一起传递给/dog路由来创建一只新的狗。

const Dog = require("./models/dog")

app.get('/dogs', (req, res) => {
	let {name, age, breed} = req.body

	let newDog = new Dog({
    	name,
    	age,
    	breed
	})

	newDog.save((err) => {
    	if (err){
        	return res.status(500).send(err.message)
    	}
    	res.status(200).send()

	})
})

当API的所有路由处理完毕后,别忘了导出 "app "对象,这样我们就可以访问服务器进行测试。

module.exports = app

在测试目录下创建一个名为dogs.js 的文件,为这个路由编写测试用例。在这个文件里面,我们要测试这个路由,看它是否响应我们所期望的结果。在这种情况下,它是确认新的狗被成功保存到数据库中。

然而,由于我们只是在测试这段代码,我们需要在测试案例结束后让数据库处于初始状态。所以,我们必须在每个测试用例结束后删除每个输入数据库的新记录。我们可以使用Mocha的afterEach钩子来实现这一点。

const chai = require("chai")
const chaiHttp = require("chai-http")
const expect = chai.expect
chai.use(chaiHttp)
const app = require('../app')
const Dog = require("./models/dog")

describe("POST /dogs", () => {

	it("should return status 200", async () => {
    	let res = await chai
        	.request(app)
        	.post('/dogs')
        	.send({name: "Charlie", age: "9", breed: "Pomerian"})
       
    	expect(res.status).to.equal(200)
       
	})

	afterEach(async () => {
    	await Dog.deleteOne({name: "Charlie"})
	})
})

我们使用async/await来处理异步路由处理功能。然后,我们使用Chai(它使用chai-http )向服务器发送一个POST请求。我们可以使用发送方法将POST请求的主体与请求一起发送。Chai的expect函数被用来断言响应与我们所期望的相等。

在测试非常复杂的API端点时,你可以遵循同样的测试过程。如果测试涉及到从数据库中输入或检索数据,确保让数据库处于初始状态,使用Mocha的beforeEach、afterEach函数来输入和删除记录。

在Sinon中使用存根

存根是用来临时替代被测试组件使用的函数的。我们使用存根来模拟一个给定函数的行为。使用存根而不是函数的真正实现的原因根据不同的情况而不同。当我们只想测试一个单元的行为时,存根在单元测试中特别有用。在集成测试中,当给定的函数还没有实现,但需要测试当前的组件时,就会使用存根。当几个组件与另一个组件捆绑在一起进行集成测试时,如果我们只想测试只有几个组件是如何一起工作的,我们可以使用存根来取代其中的一些组件。

假设上面的POST /dogs路由有一个中间件来检查发送请求的用户是否已经登录。(你可以适当地实现isLoggedIn函数)

const Dog = require("./models/dog")
const {isLoggedIn} = require('./middleware')

app.get('/dogs', isLoggedIn, (req, res) => {
	let {name, age, breed} = req.body

	let newDog = new Dog({
    	name,
    	age,
    	breed
	})

	newDog.save((err) => {
    	if (err){
        	return res.status(500).send(err.message)
    	}
    	res.status(200).send()

	})
})

但目前,我们只想测试将狗的详细信息保存到数据库并发送响应的功能。换句话说,我们暂时不想测试路由处理函数和中间件如何协同工作。所以,我们可以创建一个存根来代替中间件功能。

我们正在使用名为Sinon的NPM包来为我们的Node.js程序创建存根。在继续之前,你应该先去把这个包安装到你的应用程序中。

我们使用Sinon的callsFake函数来创建中间件存根。我们在每个测试案例之前使用beforeEach钩子创建这个存根。

const chai = require("chai")
const chaiHttp = require("chai-http")
const expect = chai.expect
chai.use(chaiHttp)
const Dog = require("./models/dog")
const sinon = require("sinon")
const middleware = require('../middleware')

describe("POST /dogs", () => {

	beforeEach(()=> {
       
    	//replace the isLoggedIn function in the middleware module with this fake function
    	loggedInStub = sinon.stub(middleware, 'isLoggedIn').callsFake((req, res, next) => {next()})
    	app = require('../app')
	})

	it("should return status 200", async () => {
    	let res = await chai
        	.request(app)
        	.post('/dogs')
        	.send({name: "Charlie", age: "9", breed: "Pomerian"})
       
    	expect(res.status).to.equal(200)
       
	})

	afterEach(() => {
    	await Dog.deleteOne({name: "Charlie"})
    	loggedInStub.restore()
	})
})

我们需要在每个测试用例之前重新导入应用程序对象,所以我们也要把它放在beforeEach钩子里面。在每个测试用例之后,我们需要恢复这个存根。

用Nock模拟HTTP请求

如果被测试的组件需要从外部API或服务中获取数据,它需要向这个API/服务发送HTTP请求,并等待响应的到来。如果我们不明确测试与该API的连接性,不向外部API发送实际请求可以减少测试时间,并保证测试不会因为网络连接不良等原因而失败。如果我们不向API发送实际请求,我们需要在测试中伪造这个请求,这种做法被称为模拟HTTP请求。

在本教程中,我们使用另一个名为Nock的NPM模块来模拟对外部API的HTTP请求。它拦截外部请求,并允许我们返回自定义响应,以适应特定的测试案例。

假设我们的API有一个路由GET/dogs/:breed 。它通过向Dog API.(https://dog.ceo/api/breed//list)发送一个请求来返回一个给定的狗品种的子品种。我们的应用服务器向Dog API发送GET请求,然后从Dog API返回的数据被送回客户端。

我们使用包,Superagent,向外部API发送一个请求。

const request = require('superagent')

app.get('/dogs/:breed', async (req, res) => {
	let breed = req.params.breed
	let result = await request
    	.get(`https://dog.ceo/api/breed/${breed}/list`)
   
	res.send(result.text)
})

它从URL中检索出品种的名称,并向Dog API发送一个GET请求,以获得子品种。

现在,我们可以测试这条路线可能存在的错误。由于我们模拟了对外部API的请求,并在测试过程中发送了一个自定义响应,我们需要将这个自定义响应保存在一个文件中,以便在我们想要的时候检索它。我把它保存在测试目录下一个名为 "response.js "的文件中。

module.exports = {
	message: [
    	"afghan",
    	"basset",
    	"blood",
    	"english",
    	"ibizan",
    	"plott",
    	"walker"
	],
	status: "success"
}

现在,让我们为上述路由编写测试案例。类似于我们之前所做的,在这里,我们定义了模拟的HTTP请求,在beforeEach钩子里面使用Nock来发送。

describe("GET /dogs/:breed", () => {

	beforeEach(() => {
    	nock('https://dog.ceo')
        	.get('/api/breed/hound/list')
        	.reply(200, response)
	})

	it("should return sub breeds of a dog breed", async () => {
    	let breed = "hound"
    	let res = await chai
        	.request(app)
        	.get(`/dogs/${breed}`)

    	expect(res.status).to.equal(200)
    	expect(res.body.status).to.equal('success')
    	expect(res.body.message).to.have.a.length(7)
	})
})

当我们向/dogs/:breed 路由发送GET请求时,Nock会拦截对Dog API的调用并返回我们保存在文件中的自定义响应。我们可以适当地改变响应的输出,以用于不同的测试案例。


总结

在今天的教程中,我们讨论了集成测试和何时应用集成测试,以及两个常见但高级的测试实践:存根和嘲弄。至此,我们关于Node.js测试的两部分系列文章结束了。我们希望你喜欢这些教程,并至少获得了如何为你的程序编写测试用例的基本概念。