测试驱动开发的简单介绍:创建对象验证器
测试驱动开发快速入门
我们的 TDD 候选:对象验证器
设置我们的环境
创建空验证器并进行初始测试
继续循环...
处理相对较大的需求变更
结论
测试驱动开发 (TDD) 看似很棒,但只有亲眼见证其实际应用,才能真正理解和领悟它。在这篇博文中,我们将使用 TDD 实现一个 JavaScript 对象验证器。
如果您学到了一些东西,请给这篇文章点赞💓、🦄或🔖!
我制作了其他易于理解的教程内容!请考虑:
- 订阅我的DevTuts 邮件列表
- 订阅我的DevTuts YouTube 频道
测试驱动开发快速入门
TDD 颠覆了许多“传统”的软件开发流程,它先编写测试,然后再编写满足测试的代码。测试通过后,代码会被重构,以确保其可读性、与代码库其他部分风格一致、高效性等等。我更喜欢用“红线、绿线、重构”来记住这个过程:
红色❌->绿色✔️->重构♻️
- 红色 ❌ - 编写测试。运行测试。由于您尚未编写任何代码来通过测试,因此新测试失败。
- 绿色✔️ - 编写通过测试(以及所有之前的测试)的代码。不必耍小聪明,只要编写代码确保测试通过就行!
- 重构♻️ - 重构你的代码!重构的原因有很多,例如效率、代码风格和可读性。重构时,请确保你的代码仍然能够通过测试。
这个过程的美妙之处在于,只要您的测试代表了您的代码的用例,您现在开发的代码就会(a)不包含任何镀金,并且(b)将在您以后每次运行测试时进行测试。
我们的 TDD 候选:对象验证器
我们的 TDD 候选对象是一个对象验证函数。该函数将接受一个对象和一些条件作为输入。最初,我们的要求如下:
- 验证器将接受两个参数:要验证的对象和标准对象
- 验证器将返回一个具有布尔
valid
属性的对象,该属性指示该对象是否有效(true
)或无效(false
)。
稍后,我们将添加一些更复杂的标准。
设置我们的环境
对于本练习,让我们创建一个新目录并安装jest
,这是我们将使用的测试框架。
mkdir object-validator
cd object-validator
yarn add jest@24.9.0
注意:您专门安装 24.9.0 版本的 jest 是为了确保您的版本与我在本教程中使用的版本相匹配。
最后一条命令会package.json
为我们创建一个文件。在这个文件中,让我们修改 scripts 部分,以便--watchAll
在运行时使用标志运行 jest yarn test
。这意味着当我们修改文件时,所有测试都会重新运行!
我们的package.json
文件现在看起来应该是这样的:
{
"scripts": {
"test": "jest"
},
"dependencies": {
"jest": "24.9.0"
}
}
接下来,创建两个文件:validator.js
和validator.test.js
。前者将包含我们验证器的代码,后者将包含我们的测试。(默认情况下,jest 会在以 结尾的文件中搜索测试.test.js
)。
创建空验证器并进行初始测试
在我们的文件中validator.js
,让我们从简单的导出开始,null
这样我们就有一些东西可以导入到我们的测试文件中。
验证器.js
module.exports = null;
验证器.测试.js
const validator = require('./validator');
初步测试
在初始测试中,我们将检查验证器是否在未提供任何条件的情况下判定对象有效。现在就来编写这个测试。
验证器.测试.js
const validator = require('./validator');
describe('validator', () => {
it('should return true for an object with no criteria', () => {
const obj = { username: 'sam21' };
expect(validator(obj, null).valid).toBe(true);
});
});
现在我们运行测试!注意,我们实际上还没有为这个validator
函数编写任何代码,所以这个测试最好失败。
千万不要跳过这一步!跳过“红绿重构”循环中的“红”部分总是很诱人,但你应该先花时间让测试失败。这样你才能测试你的测试……换句话说,你需要确认测试在应该失败的时候失败,否则就不能正确地测试你的软件。
yarn test
如果一切顺利,您应该会看到我们的测试失败了:
validator
✕ should return true for an object with no criteria (2ms)
使测试通过
既然我们已经确认测试失败了,那就让它通过吧。为此,我们只需在validator.js
文件里导出一个返回所需对象的函数即可。
验证器.js
const validator = () => {
return { valid: true };
};
module.exports = validator;
我们的测试应该仍在控制台中运行,所以如果我们看一下,我们应该会看到我们的测试现在正在通过!
validator
✓ should return true for an object with no criteria
继续循环...
让我们再添加几个测试。我们知道,我们需要根据一些条件判断一个对象是否通过。现在,我们将添加两个测试来实现这一点。
验证器.测试.js
it('should pass an object that meets a criteria', () => {
const obj = { username: 'sam123' };
const criteria = obj => obj.username.length >= 6
};
expect(validator(obj, criteria).valid).toBe(true);
});
it('should fail an object that meets a criteria', () => {
const obj = { username: 'sam12' };
const criteria = obj => obj.username.length >= 6,
};
expect(validator(obj, criteria).valid).toBe(false);
});
现在我们运行测试,确保两个新测试失败……但其中一个没有!这在 TDD 中其实很正常,而且经常是因为通用解决方案恰好符合更具体的需求而发生的。为了解决这个问题,我建议暂时更改返回的对象,以验证已经通过的测试是否确实会失败。例如,我们可以在验证函数validator.js
返回时显示所有测试都失败。{ valid: null }
validator
✕ should return true for an object with no criteria (4ms)
✕ should pass an object that meets a criteria (1ms)
✕ should fail an object that meets a criteria
obj
现在,让我们传递这些测试。我们将更新验证函数,以返回传递的结果criteria
。
验证器.js
const validator = (obj, criteria) => {
if (!criteria) {
return { valid: true };
}
return { valid: criteria(obj) };
};
module.exports = validator;
我们的测试全部通过了!现在我们应该考虑重构,但目前看来机会不多。我们继续创建测试。现在,我们要考虑到我们需要能够评估多个条件。
it('should return true if all criteria pass', () => {
const obj = {
username: 'sam123',
password: '12345',
confirmPassword: '12345',
};
const criteria = [
obj => obj.username.length >= 6,
obj => obj.password === obj.confirmPassword,
];
expect(validator(obj, criteria).valid).toBe(true);
});
it('should return false if only some criteria pass', () => {
const obj = {
username: 'sam123',
password: '12345',
confirmPassword: '1234',
};
const criteria = [
obj => obj.username.length >= 6,
obj => obj.password === obj.confirmPassword,
];
expect(validator(obj, criteria).valid).toBe(false);
});
我们的两个新测试失败了,因为我们的validator
函数不接受criteria
数组作为条件。我们可以用几种方法解决这个问题:我们可以让用户提供一个函数或一个函数数组作为条件,然后在validator
函数中处理每个条件。话虽如此,我更希望我们的validator
函数拥有一致的接口。因此,我们将条件视为数组,并根据需要修复所有之前的测试。
这是我们第一次尝试通过测试:
验证器.js
const validator = (obj, criteria) => {
if (!criteria) {
return { valid: true };
}
for (let i = 0; i < criteria.length; i++) {
if (!criteria[i](obj)) {
return { valid: false };
}
}
return { valid: true };
};
module.exports = validator;
新的测试通过了,但是之前的测试(这些测试被视为criteria
函数)失败了。让我们继续更新这些测试,确保criteria
它是一个数组。
validator.test.js(固定测试)
it('should pass an object that meets a criteria', () => {
const obj = { username: 'sam123' };
const criteria = [obj => obj.username.length >= 6];
expect(validator(obj, criteria).valid).toBe(true);
});
it('should fail an object that meets a criteria', () => {
const obj = { username: 'sam12' };
const criteria = [obj => obj.username.length >= 6];
expect(validator(obj, criteria).valid).toBe(false);
});
所有测试都通过了,代码又恢复正常了!这次,我觉得我们可以合理地重构代码了。我们回想起可以使用every
数组方法,这很符合我们团队的风格。
验证器.js
const validator = (obj, criteria) => {
if (!criteria) {
return { valid: true };
}
const valid = criteria.every(criterion => criterion(obj));
return { valid };
};
module.exports = validator;
干净多了,测试也通过了。仔细看看,由于测试的全面,我们对重构的信心有多大!
处理相对较大的需求变更
我们对验证器的现状很满意,但用户测试表明,我们确实需要能够根据验证结果支持错误消息。此外,我们需要按字段名称聚合错误消息,以便能够在正确的输入字段旁边显示给用户。
我们决定我们的输出对象需要类似于以下形状:
{
valid: false,
errors: {
username: ["Username must be at least 6 characters"],
password: [
"Password must be at least 6 characters",
"Password must match password confirmation"
]
}
}
让我们编写一些测试来适应新功能。我们很快意识到,它criteria
需要一个对象数组,而不是一个函数数组。
验证器.测试.js
it("should contain a failed test's error message", () => {
const obj = { username: 'sam12' };
const criteria = [
{
field: 'username',
test: obj => obj.username.length >= 6,
message: 'Username must be at least 6 characters',
},
];
expect(validator(obj, criteria)).toEqual({
valid: false,
errors: {
username: ['Username must be at least 6 characters'],
},
});
});
现在我们运行测试,发现最后一个测试失败了。让我们让它通过。
验证器.测试.js
const validator = (obj, criteria) => {
if (!criteria) {
return { valid: true };
}
const errors = {};
for (let i = 0; i < criteria.length; i++) {
if (!criteria[i].test(obj)) {
if (!Array.isArray(errors[criteria[i].field])) {
errors[criteria[i].field] = [];
}
errors[criteria[i].field].push(criteria[i].message);
}
}
return {
valid: Object.keys(errors).length === 0,
errors,
};
};
module.exports = validator;
现在,第一个测试和最后一个测试通过了,但其他测试都失败了。这是因为我们改变了输入的形状criteria
。
validator
✓ should return true for an object with no criteria (2ms)
✕ should pass an object that meets a criteria (3ms)
✕ should fail an object that meets a criteria
✕ should return true if all criteria pass
✕ should return false if only some criteria pass
✓ should contain a failed test's error message
既然我们知道criteria
最终测试用例的实现是正确的,那么让我们更新中间四个用例使其通过。与此同时,让我们为条件对象创建变量以便重用它们。
验证器.测试.js
const validator = require('./validator');
const usernameLength = {
field: 'username',
test: obj => obj.username.length >= 6,
message: 'Username must be at least 6 characters',
};
const passwordMatch = {
field: 'password',
test: obj => obj.password === obj.confirmPassword,
message: 'Passwords must match',
};
describe('validator', () => {
it('should return true for an object with no criteria', () => {
const obj = { username: 'sam21' };
expect(validator(obj, null).valid).toBe(true);
});
it('should pass an object that meets a criteria', () => {
const obj = { username: 'sam123' };
const criteria = [usernameLength];
expect(validator(obj, criteria).valid).toBe(true);
});
it('should fail an object that meets a criteria', () => {
const obj = { username: 'sam12' };
const criteria = [usernameLength];
expect(validator(obj, criteria).valid).toBe(false);
});
it('should return true if all criteria pass', () => {
const obj = {
username: 'sam123',
password: '12345',
confirmPassword: '12345',
};
const criteria = [usernameLength, passwordMatch];
expect(validator(obj, criteria).valid).toBe(true);
});
it('should return false if only some criteria pass', () => {
const obj = {
username: 'sam123',
password: '12345',
confirmPassword: '1234',
};
const criteria = [usernameLength, passwordMatch];
expect(validator(obj, criteria).valid).toBe(false);
});
it("should contain a failed test's error message", () => {
const obj = { username: 'sam12' };
const criteria = [usernameLength];
expect(validator(obj, criteria)).toEqual({
valid: false,
errors: {
username: ['Username must be at least 6 characters'],
},
});
});
});
如果我们检查测试,它们都通过了!
validator
✓ should return true for an object with no criteria
✓ should pass an object that meets a criteria (1ms)
✓ should fail an object that meets a criteria
✓ should return true if all criteria pass
✓ should return false if only some criteria pass (1ms)
✓ should contain a failed test's error message
看起来不错。现在我们来考虑一下如何重构。我当然不喜欢我们解决方案中的嵌套语句,而且当我们的代码仍然倾向于使用数组方法时,if
我们又回到了使用循环。以下是一个更好的版本:for
const validator = (obj, criteria) => {
const cleanCriteria = criteria || [];
const errors = cleanCriteria.reduce((messages, criterion) => {
const { field, test, message } = criterion;
if (!test(obj)) {
messages[field]
? messages[field].push(message)
: (messages[field] = [message]);
}
return messages;
}, {});
return {
valid: Object.keys(errors).length === 0,
errors,
};
};
module.exports = validator;
我们的测试仍然顺利通过,我们对重构后的代码非常满意validator
!当然,我们可以而且应该继续构建测试用例,以确保能够处理多个字段以及每个字段的多个错误,但这个探索就留给你自己去探索吧!
结论
测试驱动开发使我们能够在实际编写代码之前定义代码所需的功能。它使我们能够系统地测试和编写代码,并让我们对重构充满信心。与任何方法一样,TDD并非完美无缺。如果您未能确保测试首先失败,则很容易出错。此外,如果您编写的测试不够彻底和严格,它可能会给您一种虚假的信心。
如果您学到了一些东西,请给这篇文章点赞💓、🦄或🔖!
鏂囩珷鏉簮锛�https://dev.to/nas5w/a-gentle-introduction-to-test-driven-development-creating-an-object-validator-3kda