在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测试的两部分系列文章结束了。我们希望你喜欢这些教程,并至少获得了如何为你的程序编写测试用例的基本概念。