在我的应用程序代码中有几个地方,我必须连接到数据库并获取一些数据。 对于我的单元测试(我正在使用JestJS),我需要嘲笑它。
让我们假设这个简单的异步函数:
/getData.js
@POST
@Consumes({ MediaType.APPLICATION_JSON})
@Produces({ MediaType.APPLICATION_JSON})
@Path("add")
public Response addSuggestionAndFeedback(@Context UriInfo uriInfo, Student student) {
System.out.println(uriInfo.getAbsolutePath());
.........
}
数据库连接位于单独的文件中:
/lib/db.js
import DB from './lib/db'
export async function getData () {
const db = DB.getDB()
const Content = db.get('content')
const doc = await Content.findOne({ _id: id })
return doc
}
你可以看到,我会收到数据库并获得一个收藏。在此之后,我将收到数据。
到目前为止我的模拟尝试:
/tests/getData.test.js
import monk from 'monk'
var state = {
db: null
}
exports.connect = (options, done) => {
if (state.db) return done()
state.db = monk(
'mongodb://localhost:27017/db',
options
)
return state.db
}
exports.getDB = () => {
return state.db
}
也许这不是最好的方法......?我很高兴每一次改进。
我的问题是在哪里放置数据库模拟,因为有多个测试,每个测试都需要import { getData } from '../getData'
import DB from './lib/db'
describe('getData()', () => {
beforeEach(() => {
DB.getDB = jest.fn()
.mockImplementation(
() => ({
get: jest.fn(
() => ({
findOne: jest.fn(() => null)
})
)
})
)
})
test('should return null', () => {
const result = getData()
expect(result).toBeNull()
})
})
调用的不同模拟结果。
也许有可能创建一个函数,可以使用所需的参数调用它。
答案 0 :(得分:2)
首先,我只想指出,测试这个概念验证函数的价值似乎很低。你的代码真的没有;它是对DB客户端的所有调用。该测试基本上验证了,如果您模拟DB客户端返回null,则返回null。所以你真的只是测试你的模拟。
但是,如果您的函数在返回之前以某种方式转换数据将会很有用。 (虽然在这种情况下,我会使用自己的测试将变换放在自己的函数中,让我们回到我们开始的地方。)
因此,我建议一个解决方案可以执行您要求的,然后建议一个可以改进您的代码的解决方案。
getData()
的解决方案 - 不推荐:您可以创建一个返回模拟的函数,该模拟提供一个findOne()
,返回您指定的内容:
// ./db-test-utils
function makeMockGetDbWithFindOneThatReturns(returnValue) {
const findOne = jest.fn(() => Promise.resolve(returnValue));
return jest.fn(() => ({
get: () => ({ findOne })
}));
}
然后在您的代码文件中,在每个测试的beforeEach或之前调用DB.getDB.mockImplementation
,并传递所需的返回值,如下所示:
import DB from './db';
jest.mock('./db');
describe('testing getThingById()', () => {
beforeAll(() => {
DB.getDB.mockImplementation(makeMockGetDbWithFindOneThatReturns(null));
});
test('should return null', async () => {
const result = await getData();
expect(result).toBeNull();
});
});
这个问题真的令人兴奋,因为它是每个功能只做一件事的价值的精彩例证!
getData
似乎非常小 - 只有3行加return
语句。所以乍一看它似乎做得太多了。
然而,这个微小的函数与DB
的内部结构紧密耦合。它依赖于:
DB
- 单身人士DB.getDB()
DB.getDB().get()
DB.getDB().get().findOne()
这会产生一些负面影响:
DB
改变了它的结构,因为它可以使用第三方组件,那么每个具有这些依赖关系的函数都会中断。getDB()
和db.get('collection')
,从而导致重复的代码。这是一种可以改善事物的方法,同时使你的测试模拟更加简单。
db
而不是DB
我可能错了,但我的猜测是,每次使用DB
时,您要做的第一件事就是致电getDB()
。但是你只需要在整个代码库中进行一次调用。您可以从db
而不是./lib/db.js
导出DB
,而不是在任何地方重复该代码:
// ./lib/db.js
const DB = existingCode(); // However you're creating DB now
const dbInstance = DB.getDB();
export default dbInstance;
或者,您可以在启动函数中创建db实例,然后将其传递给DataAccessLayer类,该类将容纳所有数据库访问调用。再次只调用getDB()
一次。这样就可以避免使用单例,这样可以简化测试,因为它允许依赖注入。
// ./lib/db.js
const DB = existingCode(); // However you're creating DB now
const dbInstance = DB.getDB();
export function getCollectionByName(collectionName){
return dbInstance.get(collectionName);
}
export default dbInstance;
这个功能太琐碎了,似乎没必要。毕竟,它与它替换的代码具有相同的行数!但它消除了dbInstance
(之前db
)结构对调用代码的依赖性,同时记录了get()
的作用(从名称中不明显)。
现在,我getData
重新命名getDocById
以反映其实际效果,可能如下所示:
import { getCollectionByName } from './lib/db';
export async function getDocById(id) {
const collection = getCollectionByName('things');
const doc = await collection.findOne({ _id: id })
return doc;
}
现在你可以分别从DB:
模拟getCollectionByName// getData.test.js
import { getDocById } from '../getData'
import { getCollectionByName } from './lib/db'
jest.mock('./lib/db');
describe('testing getThingById()', () => {
beforeEach(() => {
getCollectionByName.mockImplementation(() => ({
findOne: jest.fn(() => Promise.resolve(null))
}));
});
test('should return null', async () => {
const result = await getDocById();
expect(result).toBeNull();
});
});
这只是一种方法,可以采取更进一步的措施。例如,我们可以导出findOneDocById(collectionName, id)
和/或findOneDoc(collectionName, searchObject)
,以使我们的模拟和findOne()
调用更简单。