使用 Sinon 模拟服务模块
Mocking with Sinon against a service module
我已经到了单元测试的阶段,老实说,网上各种不同的例子让我感到困惑。我对Mocha & Chai很了解,但Sinon是另外一回事。
所以我有一个我认为非常简单的设置。我有一个调用控制器的 POST 路由。这个控制器是这样的(去掉了一些基本的验证码)
const { createUser } = require('../services/user.service');
const apiResponse = require('../helpers/apiResponse');
const postUser = async (req, res) => {
const user = {
account_id: req.body.id,
status: req.body.status,
created_at: new Date(),
updated_at: new Date(),
};
const result = await createUser(user);
return apiResponse.successResponseWithData(res, 'User added.', result.affectedRows);
} catch (err) {
return apiResponse.errorResponse(res, err);
}
};
module.exports = {
postUser,
};
所以它真正做的就是验证,然后使用请求创建一个用户对象并将其传递给服务 class。此服务 class 只是将数据传递到数据库 class。
const { addUserToDb } = require('../database/user.db');
const createUser = async (user) => {
try {
const createdUser = await addUserToDb(user);
return createdUser;
} catch (err) {
throw new Error(err);
}
};
module.exports = {
createUser,
};
我不会展示数据库class因为我首先要关注的是控制器,然后我希望自己可以完成剩下的事情。
所以据我了解,我应该测试功能。如果一个函数进行外部调用,我应该监视、模拟、存根该调用吗?如果依赖项之一,我应该只监视、模拟或存根此函数依赖项
有自己的依赖(像上面的服务模块有数据库调用依赖),这应该在另一个测试中执行?抱歉,请问几个问题来帮助我理解。
无论如何,我创建了一个 user.controller.test.js 文件。我还没有深入了解它,但这就是我目前所拥有的
const chai = require('chai');
const sinon = require('sinon');
const { expect } = chai;
const faker = require('faker');
const controller = require('../controllers/user.controller');
const service = require('../services/user.service');
const flushPromises = () => new Promise(setImmediate);
describe('user.controller', () => {
describe('postUser', () => {
beforeEach(() => {
//I see a lot of code use a beforeEach, what should I be doing here?
});
it('should create a user when account_id and status params are provided', async () => {
const req = {
body: { account_id: faker.datatype.uuid(), status: 'true' },
};
const stubValue = {
id: faker.datatype.id(),
account_id: faker.datatype.uuid(),
status: 'true',
created_at: faker.date.past(),
updated_at: faker.date.past(),
};
});
});
});
老实说,我不知道应该在这里测试什么。根据我的理解,我需要模拟我认为的服务模块。
有人可以提供一些关于我应该在这个测试中做什么的见解吗?
非常感谢
更新
感谢您的详细回复,我已经设法让间谍工作,这是向前迈出的一步。所以我想对我的服务模块 createUser
方法进行测试。
您可以看到我的 createUser
方法将用户对象作为参数并将其传递给数据库模块,然后将其插入数据库,然后返回用户对象。
所以在测试我的服务时 class,我需要模拟这个对我的数据库模块的调用。
const chai = require('chai');
const sinon = require('sinon');
const { expect } = chai;
const faker = require('faker');
const service = require('../services/user.service');
const database = require('../database/user.db');
describe('user.service', () => {
describe('createUser', () => {
it('should create a user when user object is provided', async () => {
const user = {
id: faker.datatype.string(),
status: 'true',
created_at: faker.date.past(),
updated_at: faker.date.past(),
};
const expectedUser = {
id: user.id,
status: user.status,
created_at: user.created_at,
updated_at: user.updated_at,
};
const mockedDatabase = sinon.mock(database);
mockedDatabase.expects('addUserToDb').once().withArgs(expectedUser);
await service.createUser(user);
mockedDatabase.verify();
mockedDatabase.restore();
});
});
});
当我测试这个时,我似乎得到了这个响应,而且它似乎仍在将记录插入我的数据库。
ExpectationError: Expected addUserToDb({
id: 'yX7AX\J&gf',
status: 'true',
created_at: 2020-06-03T03:10:23.472Z,
updated_at: 2020-05-24T14:44:14.749Z
}, '[...]') once (never called)
at Object.fail (node_modules\sinon\lib\sinon\mock-expectation.js:314:25)
你知道我做错了什么吗?
谢谢
在我尝试之前,我想建议在所有地方删除 try/catch
块,我假设你在你的 Node 应用程序中使用 expressJs,为此,看看 express-promise-router
因为使用那个路由器(而不是默认路由器)会自动 catch
它抛出的任何东西,你只需要专注于代码...
以你的例子为例,你会写:
const { addUserToDb } = require('../database/user.db');
const createUser = async (user) => addUserToDb(user);
module.exports = {
createUser,
};
和
const { createUser } = require('../services/user.service');
const apiResponse = require('../helpers/apiResponse');
const postUser = async (req, res) => {
const { id: account_id, status } = res.body;
const result = await createUser({ account_id, status }); // set the date in the fn
return apiResponse.successResponseWithData(res, 'User added.', result.affectedRows);
};
module.exports = {
postUser,
};
如果出现错误并且在路线上的某个地方抛出错误,您将在响应中收到一条带有错误的好消息
关于代码本身,阅读起来似乎更清晰 - 请记住,代码是为人类编写的,机器甚至不关心你如何命名变量
现在,关于测试......我倾向于将事情分成 3 个部分
- 单元测试:函数本身,单个函数,如验证、助手等
- 集成测试:当您调用 API 端点时,应该 returned
- GUI 测试(或end-to-end/e2e):存在 GUI 时应用,暂时跳过此
所以在你的情况下,首先要确定的是你在测试什么......然后从小块开始(单元测试)并向上移动到确保所有内容都粘合的块一起 (e2e)
So all it really does is validate, and then creates a user object with the req and pass that to a service class. This services class does nothing more than pass the data to a database class.
似乎是一个很好的开始方式,所以它“验证”...让我们测试我们的验证,让我们通过 null
、undefined
、string
,当你想要的只是 int
依此类推,直到我们非常清楚无论它通过什么,我们都会正确地回复有无错误
注意 我倾向于使用 OpenAPI 规范,这对我来说更容易,因为它提供了 2 个东西
是的,我总是测试一些验证以确保它按预期工作,即使我 100% 信任该工具
So from what I understand, I should be testing functions.
好吧,应用程序是一组功能,所以一切都很好
If a function makes an external call, I should spy, mock, stub that call?
我会尽我所能解释什么是 Sinon 中的间谍、存根和模拟,请温柔点
间谍
它们告诉我们有关函数调用的信息,例如调用次数、参数、return 值等等 - 它们有两种类型,匿名间谍或在我们的代码中包装方法的间谍
function testMyCallback(callback) { callback(); }
describe('testMyCallback fn', function() {
it('should call the callback', function() {
const callbackSpy = sinon.spy(); // anonymous spy - no arguments
testMyCallback(callbackSpy);
expect(callbackSpy).to.have.been.calledOnce;
});
});
const user = {
setNname: function(name) {
this.name = name;
}
}
describe('setname fn', function() {
it('should be called with name', function() {
const setNameSpy = sinon.spy(user, 'setName'); // wrap method spy
user.setName('Katie');
expect(setNameSpy).to.have.been.calledOnce;
expect(setNameSpy).to.have.been.valledWith('Katie');
setNameSpy.restore(); // to remove the Spy and prevent future errors
});
});
存根
是强力间谍,因为它们具有间谍的所有功能,但它们替换了目标函数,它们具有可以return特定值或抛出一个特定的异常和更多
它们非常适合用于解决您关于外部调用的问题,因为它们取代了调用(因此您可以模拟调用行为而永远不会使用原始调用)
最简单的示例是:
function isAdult(age) {
return age > 21;
}
describe('Sinon Stub Example', () => {
it('should pass', (done) => {
const isAdult = sinon.stub().returns('something');
isAdult(0).should.eql('something');
isAdult(0).should.not.eql(false);
done();
});
});
我们已经对我们的函数进行了 STUB,并明确表示它是一个 return 是一个字符串 something
的“函数”...现在,我们永远不需要去函数本身,因为我们有 STUB 它,我们已经用我们自己的
替换了真实的行为
在我们的集成测试中调用我们的 API 应用程序时使用 STUB 的另一个例子
describe('when we stub our API call', () => {
beforeEach(() => {
this.get = sinon.stub(request, 'get'); // stub "request.get" function
});
afterEach(() => {
request.get.restore(); // remove our power-spy
});
describe('GET /api/v1/accounts', () => {
const responseObject = {
status: 200,
headers: {
'content-type': 'application/json'
}
};
const responseBody = {
status: 'success',
data: [
{
accountId: 1,
status: 'active'
},
{
accountId: 2,
status: 'disabled'
}
]
};
it('should return all accounts', (done) => {
// the 3 objects of our callback (err, res, body)
this.get.yields(null, responseObject, JSON.stringify(responseBody));
request.get(`${base}/api/v1/movies`, (err, res, body) => {
expect(res.statusCode).to.be.eql(200);
expect(res.headers['content-type']).to.contain('application/json');
body = JSON.parse(body);
expect(body).to.be.an('array').that.includes(2);
done();
});
});
});
});
你也可以存根 axios,但是你需要一个新的库,moxios, or proxyquire 或更多...
模拟
有点类似于存根(我们的 Power-Spies),但它们可用于替换整个对象并改变它们的行为,它们主要用于需要从单个对象存根多个函数时 - 如果只需替换单个函数,存根更容易使用
模拟会使事情变得过于简单,您可能会在不知不觉中破坏您的应用程序,所以请注意...
一个通常使用的例子是
function setupNewAccount(info, callback) {
const account = {
account_id: info.id,
status: info.status,
created_at: new Date(),
updated_at: new Date()
};
try { Database.save(account, callback); }
catch (err) { callback(err); }
}
describe('setupNewAccount', function() {
it('', function() {
const account = { account_id: 1, status: 'active' };
const expectedAccount = {
account_id: account.id, status: account.status
};
const database = sinon.mock(Database);
database.expectes('save').once().withArgs(expectedAccount);
setupNewAccount(account, function() {});
database.verify();
database.restore();
});
});
我们会一直忘记的是 .restore()
部分,为此,有一个名为 sinon-test
的包(还有一个...)会在测试结束时自动清理
我只是希望它能帮助你解决一些问题,现在更清楚了
顺便说一句,对于 HTTP 请求存根,我使用 nock
因为我认为它比 Sinon 更容易阅读和使用,特别是对于第一次阅读代码并且没有任何 Sinon 经验的人或诺克...
我已经到了单元测试的阶段,老实说,网上各种不同的例子让我感到困惑。我对Mocha & Chai很了解,但Sinon是另外一回事。
所以我有一个我认为非常简单的设置。我有一个调用控制器的 POST 路由。这个控制器是这样的(去掉了一些基本的验证码)
const { createUser } = require('../services/user.service');
const apiResponse = require('../helpers/apiResponse');
const postUser = async (req, res) => {
const user = {
account_id: req.body.id,
status: req.body.status,
created_at: new Date(),
updated_at: new Date(),
};
const result = await createUser(user);
return apiResponse.successResponseWithData(res, 'User added.', result.affectedRows);
} catch (err) {
return apiResponse.errorResponse(res, err);
}
};
module.exports = {
postUser,
};
所以它真正做的就是验证,然后使用请求创建一个用户对象并将其传递给服务 class。此服务 class 只是将数据传递到数据库 class。
const { addUserToDb } = require('../database/user.db');
const createUser = async (user) => {
try {
const createdUser = await addUserToDb(user);
return createdUser;
} catch (err) {
throw new Error(err);
}
};
module.exports = {
createUser,
};
我不会展示数据库class因为我首先要关注的是控制器,然后我希望自己可以完成剩下的事情。
所以据我了解,我应该测试功能。如果一个函数进行外部调用,我应该监视、模拟、存根该调用吗?如果依赖项之一,我应该只监视、模拟或存根此函数依赖项 有自己的依赖(像上面的服务模块有数据库调用依赖),这应该在另一个测试中执行?抱歉,请问几个问题来帮助我理解。
无论如何,我创建了一个 user.controller.test.js 文件。我还没有深入了解它,但这就是我目前所拥有的
const chai = require('chai');
const sinon = require('sinon');
const { expect } = chai;
const faker = require('faker');
const controller = require('../controllers/user.controller');
const service = require('../services/user.service');
const flushPromises = () => new Promise(setImmediate);
describe('user.controller', () => {
describe('postUser', () => {
beforeEach(() => {
//I see a lot of code use a beforeEach, what should I be doing here?
});
it('should create a user when account_id and status params are provided', async () => {
const req = {
body: { account_id: faker.datatype.uuid(), status: 'true' },
};
const stubValue = {
id: faker.datatype.id(),
account_id: faker.datatype.uuid(),
status: 'true',
created_at: faker.date.past(),
updated_at: faker.date.past(),
};
});
});
});
老实说,我不知道应该在这里测试什么。根据我的理解,我需要模拟我认为的服务模块。
有人可以提供一些关于我应该在这个测试中做什么的见解吗?
非常感谢
更新
感谢您的详细回复,我已经设法让间谍工作,这是向前迈出的一步。所以我想对我的服务模块 createUser
方法进行测试。
您可以看到我的 createUser
方法将用户对象作为参数并将其传递给数据库模块,然后将其插入数据库,然后返回用户对象。
所以在测试我的服务时 class,我需要模拟这个对我的数据库模块的调用。
const chai = require('chai');
const sinon = require('sinon');
const { expect } = chai;
const faker = require('faker');
const service = require('../services/user.service');
const database = require('../database/user.db');
describe('user.service', () => {
describe('createUser', () => {
it('should create a user when user object is provided', async () => {
const user = {
id: faker.datatype.string(),
status: 'true',
created_at: faker.date.past(),
updated_at: faker.date.past(),
};
const expectedUser = {
id: user.id,
status: user.status,
created_at: user.created_at,
updated_at: user.updated_at,
};
const mockedDatabase = sinon.mock(database);
mockedDatabase.expects('addUserToDb').once().withArgs(expectedUser);
await service.createUser(user);
mockedDatabase.verify();
mockedDatabase.restore();
});
});
});
当我测试这个时,我似乎得到了这个响应,而且它似乎仍在将记录插入我的数据库。
ExpectationError: Expected addUserToDb({
id: 'yX7AX\J&gf',
status: 'true',
created_at: 2020-06-03T03:10:23.472Z,
updated_at: 2020-05-24T14:44:14.749Z
}, '[...]') once (never called)
at Object.fail (node_modules\sinon\lib\sinon\mock-expectation.js:314:25)
你知道我做错了什么吗?
谢谢
在我尝试之前,我想建议在所有地方删除 try/catch
块,我假设你在你的 Node 应用程序中使用 expressJs,为此,看看 express-promise-router
因为使用那个路由器(而不是默认路由器)会自动 catch
它抛出的任何东西,你只需要专注于代码...
以你的例子为例,你会写:
const { addUserToDb } = require('../database/user.db');
const createUser = async (user) => addUserToDb(user);
module.exports = {
createUser,
};
和
const { createUser } = require('../services/user.service');
const apiResponse = require('../helpers/apiResponse');
const postUser = async (req, res) => {
const { id: account_id, status } = res.body;
const result = await createUser({ account_id, status }); // set the date in the fn
return apiResponse.successResponseWithData(res, 'User added.', result.affectedRows);
};
module.exports = {
postUser,
};
如果出现错误并且在路线上的某个地方抛出错误,您将在响应中收到一条带有错误的好消息
关于代码本身,阅读起来似乎更清晰 - 请记住,代码是为人类编写的,机器甚至不关心你如何命名变量
现在,关于测试......我倾向于将事情分成 3 个部分
- 单元测试:函数本身,单个函数,如验证、助手等
- 集成测试:当您调用 API 端点时,应该 returned
- GUI 测试(或end-to-end/e2e):存在 GUI 时应用,暂时跳过此
所以在你的情况下,首先要确定的是你在测试什么......然后从小块开始(单元测试)并向上移动到确保所有内容都粘合的块一起 (e2e)
So all it really does is validate, and then creates a user object with the req and pass that to a service class. This services class does nothing more than pass the data to a database class.
似乎是一个很好的开始方式,所以它“验证”...让我们测试我们的验证,让我们通过 null
、undefined
、string
,当你想要的只是 int
依此类推,直到我们非常清楚无论它通过什么,我们都会正确地回复有无错误
注意 我倾向于使用 OpenAPI 规范,这对我来说更容易,因为它提供了 2 个东西
是的,我总是测试一些验证以确保它按预期工作,即使我 100% 信任该工具
So from what I understand, I should be testing functions.
好吧,应用程序是一组功能,所以一切都很好
If a function makes an external call, I should spy, mock, stub that call?
我会尽我所能解释什么是 Sinon 中的间谍、存根和模拟,请温柔点
间谍
它们告诉我们有关函数调用的信息,例如调用次数、参数、return 值等等 - 它们有两种类型,匿名间谍或在我们的代码中包装方法的间谍
function testMyCallback(callback) { callback(); }
describe('testMyCallback fn', function() {
it('should call the callback', function() {
const callbackSpy = sinon.spy(); // anonymous spy - no arguments
testMyCallback(callbackSpy);
expect(callbackSpy).to.have.been.calledOnce;
});
});
const user = {
setNname: function(name) {
this.name = name;
}
}
describe('setname fn', function() {
it('should be called with name', function() {
const setNameSpy = sinon.spy(user, 'setName'); // wrap method spy
user.setName('Katie');
expect(setNameSpy).to.have.been.calledOnce;
expect(setNameSpy).to.have.been.valledWith('Katie');
setNameSpy.restore(); // to remove the Spy and prevent future errors
});
});
存根
是强力间谍,因为它们具有间谍的所有功能,但它们替换了目标函数,它们具有可以return特定值或抛出一个特定的异常和更多
它们非常适合用于解决您关于外部调用的问题,因为它们取代了调用(因此您可以模拟调用行为而永远不会使用原始调用)
最简单的示例是:
function isAdult(age) {
return age > 21;
}
describe('Sinon Stub Example', () => {
it('should pass', (done) => {
const isAdult = sinon.stub().returns('something');
isAdult(0).should.eql('something');
isAdult(0).should.not.eql(false);
done();
});
});
我们已经对我们的函数进行了 STUB,并明确表示它是一个 return 是一个字符串 something
的“函数”...现在,我们永远不需要去函数本身,因为我们有 STUB 它,我们已经用我们自己的
在我们的集成测试中调用我们的 API 应用程序时使用 STUB 的另一个例子
describe('when we stub our API call', () => {
beforeEach(() => {
this.get = sinon.stub(request, 'get'); // stub "request.get" function
});
afterEach(() => {
request.get.restore(); // remove our power-spy
});
describe('GET /api/v1/accounts', () => {
const responseObject = {
status: 200,
headers: {
'content-type': 'application/json'
}
};
const responseBody = {
status: 'success',
data: [
{
accountId: 1,
status: 'active'
},
{
accountId: 2,
status: 'disabled'
}
]
};
it('should return all accounts', (done) => {
// the 3 objects of our callback (err, res, body)
this.get.yields(null, responseObject, JSON.stringify(responseBody));
request.get(`${base}/api/v1/movies`, (err, res, body) => {
expect(res.statusCode).to.be.eql(200);
expect(res.headers['content-type']).to.contain('application/json');
body = JSON.parse(body);
expect(body).to.be.an('array').that.includes(2);
done();
});
});
});
});
你也可以存根 axios,但是你需要一个新的库,moxios, or proxyquire 或更多...
模拟
有点类似于存根(我们的 Power-Spies),但它们可用于替换整个对象并改变它们的行为,它们主要用于需要从单个对象存根多个函数时 - 如果只需替换单个函数,存根更容易使用
模拟会使事情变得过于简单,您可能会在不知不觉中破坏您的应用程序,所以请注意...
一个通常使用的例子是
function setupNewAccount(info, callback) {
const account = {
account_id: info.id,
status: info.status,
created_at: new Date(),
updated_at: new Date()
};
try { Database.save(account, callback); }
catch (err) { callback(err); }
}
describe('setupNewAccount', function() {
it('', function() {
const account = { account_id: 1, status: 'active' };
const expectedAccount = {
account_id: account.id, status: account.status
};
const database = sinon.mock(Database);
database.expectes('save').once().withArgs(expectedAccount);
setupNewAccount(account, function() {});
database.verify();
database.restore();
});
});
我们会一直忘记的是 .restore()
部分,为此,有一个名为 sinon-test
的包(还有一个...)会在测试结束时自动清理
我只是希望它能帮助你解决一些问题,现在更清楚了
顺便说一句,对于 HTTP 请求存根,我使用 nock
因为我认为它比 Sinon 更容易阅读和使用,特别是对于第一次阅读代码并且没有任何 Sinon 经验的人或诺克...