在云计算普及的今天,Cloud Native 开发越来越流行。所谓云原生开发,就是从开发的第一天开始,就依赖云环境和云资源,不再有本地开发环境。这给 TDD 普及度本就很低的现状,又带来了更多的挑战,甚至导致了资深的开发人员,在云原生开发中也放弃了 TDD 实践。
本文以一个非常具体的例子,来戳破云原生这个纸老虎,让 TDD 的实践者更有信心,在云原生开发中,不再有 TDD 很难的错觉和心理恐惧。
问题
在使用 jest 写测试时,如果待测试对象中引用了 aws sdk,那么在直接运行测试时会碰到这个问题:
shell ConfigError: Missing region in config
原因
待测试对象中的方法,使用 aws sdk 时,会向 AWS 服务发送 HTTP 调用请求,由于没有传递 region,导致 AWS 服务报错,最终响应到了客户端这个错误信息。
解决方案
可以通过 jest 将 aws-sdk mock 掉,以避免在测试过程中真正去调用 AWS api。
具体做法
首先,要将 aws-sdk 整个地 mock 掉:
typescript export const mockAwsSdk = { DynamoDb: jest.fn(), CloudWatchLogs: jest.fn(), SQS: jest.fn() Lambda: jest.fn() }
jest.mock(aws-sdk, () => mockAwsSdk)
然后,针对待测对象的实际调用,提供针对性的 mock。比如待测对象使用了 DynamoDb 中的几乎所有方法,那么在 mock 时,可以使用一个数组来做为 DynamoDb 的一个替代。于是,可以实现以下的 mockDynamoDb(注意,使用了 items: [] 做为初始表格):
typescript // 自定义一个查询函数,在后面模拟搜索时可以用到。注意这里是一个示例,具体的实现可以根据待测对象使用到的 DynamoDb 的具体 Schema 任意改写 const queryByOperationKeyAndScenarioType = (opKey, scType) => (item) => (item.operationKey.S === opKey.S || item.operationKey.S === opKey.S.split(|)[0] + |*) && (scType ? item.scenarioType.S === scType.S : true)
export const mockDynamoDB = { createTable: jest.fn().mockImplementation(() => { return { promise: () => { return Promise.resolve() }, } }), updateTimeToLive: jest.fn().mockImplementation(() => { return { promise: () => { return Promise.resolve() }, } }), items: [], putItem: jest.fn().mockImplementation((params) => { // 假定params至多前4个Key是主键 const existed = mockDynamoDB.items.findIndex((item) => { const [pk1, pk2, pk3, pk4] = Object.keys(params.Item)
return [pk1, pk2, pk3, pk4]
.filter((pk) => !!pk)
.reduce((prev, next) => prev && JSON.stringify(item[next]) === JSON.stringify(params.Item[next]), true)
})
if (existed === -1) {
mockDynamoDB.items.push(params.Item)
} else {
mockDynamoDB.items[existed] = params.Item
}
return mockDynamoDB
}),
getItem: jest.fn().mockImplementation((params) => {
// 根据传入的参数从数组中筛选出符合条件的第一条记录
const [res] = mockDynamoDB.items.filter((item) =>
Object.keys(params.Key).reduce(
(prev, next) => prev && JSON.stringify(params.Key[next]) === JSON.stringify(item[next]),
true
)
)
return { promise: () => Promise.resolve({ Item: res }) }
}),
query: jest.fn().mockImplementation((params) => {
return {
promise: () => {
return Promise.resolve({
// 用自定义的筛选函数,当然,你也可以定义多个,根据需要使用不同的筛选函数
Items: mockDynamoDB.items.filter(
queryByOperationKeyAndScenarioType(
params.ExpressionAttributeValues[:opKey],
params.ExpressionAttributeValues[:scType]
)
),
})
},
}
}),
scan: jest.fn().mockImplementation((params) => {
return {
promise: () => {
// 可以直接返回 items,或者不直接依赖它的结果的话,直接 resolve
return Promise.resolve()
},
}
}),
promise: jest.fn().mockReturnValue(Promise.resolve()),
batchWriteItem: jest.fn().mockImplementation((params) => {
// 根据需要将传入的参数写入数组
for (const table in params.RequestItems) {
const item = params.RequestItems[table]
mockDynamoDB.items.push(item[0].PutRequest.Item)
}
return {
promise: () => {
return Promise.resolve({})
},
}
}),
}
以上是一个待测对象使用了 DynamoDb 的例子,如果使用了 SQS,也可以类似地 mock 一个 SQS。在有了 mockDynamoDb 后,就可以把这个 mock 对象灌入最早的 jest mock 过的 aws-sdk 了:
typescript mockAwsSdk.DynamoDb = jest.fn(() => mockDynamoDb)
注意事项
jest.mock(aws-sdk, ...) 这一行,需要放在引入待测对象之前,即在待测对象在真实引用 aws-sdk 前,把 aws-sdk 替换掉。
效果
于是,测试就可以愉快的进行了,既可以测试自己的业务逻辑,但是又不真正调用 AWS Api:
总结
测试驱动开发是一个以终为始的做事方法,是10倍程序员工作方法之一。但是为什么难以推广呢?既然是 10 倍工作效率,但业界采用比例为什么仍然很低呢?原因之一就是在实践过程中有各种拦路虎,比如让人觉得难以 mock。实际上,mock 方法有很多,本文对 aws sdk 这个具体的场景,提供了一种可行的 mock 方案,希望扫清大家在引用了 aws sdk 的开发中的 TDD 障碍。