我的测试之旅
我写的第一个程序是在高中的TI-83上。我并不是想写干净的代码。我只是想让它工作。从那时起,我就知道了测试的价值。它使我的生活轻松了许多。然而,当我在写测试方面变得越来越有规律时,我又遇到了一个问题,那就是我有很多难以维护的测试。在过去的两年里,我一直在努力编写高质量的测试,这些测试重量较轻,易于维护。今天,我将分享纯函数如何帮助你的测试变得更干净。
纯函数
纯函数是指其返回值只基于输入参数的函数。此外,它不执行任何副作用(通常是I/O)。常见的不能在纯函数中发生的操作有:从数据库中读取,写到控制台,甚至读取全局变量。
这些函数很容易推理。它们也很容易测试,因为它们不需要任何测试替身。让我们看一个简单的例子。
function increment(number) {
return number + 1;
}
和简单的测试。
test('increment should add one to the number', () => {
expect(increment(1)).toEqual(2)
})
让我们来看看一个不纯的函数。
async function increment() {
let number = await repo.getNumberFromDatabase();
number += 1;
await repo.setNumberInDatabase(number);
return number;
}
这不是纯粹的,因为它从数据库中获取数据。其结果会根据外部状态而变化。为了进行单元测试,我们必须监视我们的数据库函数并对其行为进行预编程。
describe('increment', () => {
test('when successful, should add one to the number in the database', () => {
const mockRepo = mock(repo)
mockRepo.when('getNumberFromDatabase').returns(1);
mockRepo.when('setNumberInDatabase').returns();
await increment();
expect(mockRepo.setNumberInDatabase.calls[0][0]).toEqual(2);
})
test('when fetching the value from the database fails', () => {
// ...
})
})
把I/O推到你的应用程序的边缘
你的应用程序肯定会有I/O,写集成测试或使用测试替身并没有错。问题是当我们把不需要的函数变得不纯时。让我们看一个例子。
function getAverageTransactionAmountForAccount(accountId) {
const sql = 'SELECT * FROM transactions WHERE account_id = $1';
const result = await db.query(sql, [accountId]);
const amounts = result.rows.map(row => row.amount);
const sum = amounts.reduce((amount, sum) => amount + sum, 0);
return sum / amounts.length;
}
我们实际上是在做三件事。
- 建立一个查询(可以是纯的)
- 执行查询(I/O,不纯)。
- 转换结果(可以是纯的)。
你也应该测试你的应用程序,要么用带有测试双击的单元测试,要么用端到端的测试。我建议采用后者。由于单元测试已经涵盖了所有的排列组合,你不需要再用端到端测试来涵盖它们。我通常把它保持在最低限度。
让我们把这段代码分成这三个部分。
// ----------------------
// application.js
// ----------------------
function getAverageTransactionAmountForAccount(accountId) {
const query = buildSelectionTransactionsQuery(accountId);
const result = await db.query(query);
return getAverageTransactionAmount(result.rows);
}
// ----------------------
// transforms.js
// ----------------------
function buildSelectionTransactionsQuery(accountId) {
return {
sql: 'SELECT * FROM transactions WHERE account_id = $1',
params: [accountId]
};
}
function getAverageTransactionAmount(transactionRows) {
const amounts = transactionRows.map(row => row.amount);
const sum = amounts.reduce((amount, sum) => amount + sum, 0);
return sum / amounts.length;
}
- 构建查询。现在这部分包含在
buildSelectionTransactionsQuery,并且是纯的。尽管它是纯粹的,你可能想为它写一个集成测试,以确保查询做你所期望的。 - 执行查询(I/O)。这包含在
db.query。 - 转换结果。现在包含在
getAverageTransactionAmount。它包含我们的主要计算,现在更容易测试。
这对于有更多排列组合的复杂计算来说特别有帮助。
总结
有许多做法可以帮助你编写可测试的Javascript代码。对我来说,通过将I/O推到边缘而偏爱纯函数是一个很容易的胜利。这样的思考方式也是进入函数式编程的第一步。