如何为调用两个依赖函数的getAppts函数编写正确单元测试?
问题:如何正确测试getAppts函数?
我需要测试当前模块里的getAppts函数,这个函数调用了另外两个函数。请问评估getAppts代码覆盖率的正确方式是什么?是否需要在单元测试中把db.getDatabase()和fetchAppts()都设为stub?我现在的单元测试写得很不完整,甚至可能完全错误。
业务代码
'use strict' const db = require('../db/db') const log = require('../utils/logging')(__filename) const { fetchAppts, fetchApptDetails } = require('../db/dao/appointment-dao') async function getAppts (queryParams) { log.debug('getAppts(): %j', queryParams) const knex = db.getDatabase() const appointments = await fetchAppts(queryParams, knex) return appointments } async function getApptDetails (demoApptId) { log.debug('getApptDetails(): %j', demoApptId) const knex = db.getDatabase() const apptDetails = await fetchApptDetails(demoApptId, knex) return apptDetails } module.exports = { getAppts, getApptDetails }
当前单元测试代码
'use strict' const sinon = require('sinon') const chai = require('chai') const chaiAsPromised = require('chai-as-promised') const dirtyChai = require('dirty-chai') const sinonChai = require('sinon-chai') const { expect } = require('chai') const proxyquire = require('proxyquire') const apptsResponse = require('../../data/get-appts-response.json') chai.use(dirtyChai) chai.use(sinonChai) chai.use(chaiAsPromised) chai.should() describe.only('appointment-service.js', () => { let apptService let apptDaoStub let dbStub beforeEach(() => { dbStub = { getDatabase: sinon.stub() } apptDaoStub = { fetchAppts: sinon.stub() } apptService = proxyquire('../../../services/appointment-service', { '../db/db': dbStub, '../db/dao/appointment-dao': apptDaoStub }) }) afterEach(() => sinon.restore()) describe('getAppts', () => { it.only('should get appointments', async () => { const appts = await apptService.getAppts({}) dbStub.getDatabase.should.have.been.calledTwice }) }) })
解答
1. 是否需要Stub这两个函数?
必须Stub。单元测试的核心是隔离被测试函数的逻辑,只验证getAppts本身的行为:它是否正确调用了依赖、是否正确返回结果。db.getDatabase()是获取数据库连接,fetchAppts()是实际的数据查询逻辑,这两个都属于外部依赖,Stub后可以避免真实调用数据库,也能精准控制依赖的返回值,确保测试的稳定性和可预测性。
2. 当前测试的问题
你的测试存在几个关键问题:
- 没有给Stub设置返回值:
fetchAppts是异步函数,不设置返回值的话,await会一直等待,导致测试超时或返回undefined;db.getDatabase()也需要返回一个模拟的knex实例,否则fetchAppts会收到无效参数。 - 断言错误:业务代码里
getAppts只调用了一次db.getDatabase(),但你断言它被调用了两次,这必然失败。 - 未验证返回值:没检查
getAppts的返回结果是否符合预期,这是单元测试的核心目标之一。
3. 修正后的测试示例
'use strict' const sinon = require('sinon') const chai = require('chai') const chaiAsPromised = require('chai-as-promised') const dirtyChai = require('dirty-chai') const sinonChai = require('sinon-chai') const proxyquire = require('proxyquire') const apptsResponse = require('../../data/get-appts-response.json') chai.use(dirtyChai) chai.use(sinonChai) chai.use(chaiAsPromised) chai.should() describe('appointment-service.js', () => { let apptService let apptDaoStub let dbStub const mockKnex = {} // 模拟knex实例 beforeEach(() => { dbStub = { getDatabase: sinon.stub().returns(mockKnex) // 设置返回模拟knex } apptDaoStub = { fetchAppts: sinon.stub().resolves(apptsResponse) // 设置异步返回预设数据 } apptService = proxyquire('../../../services/appointment-service', { '../db/db': dbStub, '../db/dao/appointment-dao': apptDaoStub }) }) afterEach(() => sinon.restore()) describe('getAppts', () => { it('should call dependencies correctly and return appointments', async () => { const testQueryParams = { status: 'active' } const result = await apptService.getAppts(testQueryParams) // 验证db.getDatabase被调用一次 dbStub.getDatabase.should.have.been.calledOnce // 验证fetchAppts被传入正确的参数 apptDaoStub.fetchAppts.should.have.been.calledWithExactly(testQueryParams, mockKnex) // 验证返回值符合预期 result.should.deep.equal(apptsResponse) }) }) })
4. 代码覆盖率评估方式
要确保getAppts的所有代码路径都被覆盖:
- 正常流程:验证函数能正确调用依赖并返回结果(就是上面的测试场景)。
- 异常流程:添加测试用例,模拟
fetchAppts抛出错误,验证getAppts是否正确传递错误(比如断言函数会抛出对应的异常)。 - 使用覆盖率工具(如
nyc)运行测试,查看getAppts的代码行、分支是否都被覆盖,确保没有遗漏的逻辑。
内容的提问来源于stack exchange,提问作者Moon Shine
相关产品推荐
相关产品推荐

