测试驱动开发的简单介绍:创建对象验证器 测试驱动开发的快速入门 我们的 TDD 候选:对象验证器 设置我们的环境 创建空的验证器和初始测试 继续循环...处理相对较大的需求变更 结论

2025-06-10

测试驱动开发的简单介绍:创建对象验证器

测试驱动开发快速入门

我们的 TDD 候选:对象验证器

设置我们的环境

创建空验证器并进行初始测试

继续循环...

处理相对较大的需求变更

结论

测试驱动开发 (TDD) 看似很棒,但只有亲眼见证其实际应用,才能真正理解和领悟它。在这篇博文中,我们将使用 TDD 实现一个 JavaScript 对象验证器。

如果您学到了一些东西,请给这篇文章点赞💓、🦄或🔖!


我制作了其他易于理解的教程内容!请考虑:


测试驱动开发快速入门

TDD 颠覆了许多“传统”的软件开发流程,它先编写测试,然后再编写满足测试的代码。测试通过后,代码会被重构,以确保其可读性、与代码库其他部分风格一致、高效性等等。我更喜欢用“红线、绿线、重构”来记住这个过程:

红色❌->绿色✔️->重构♻️

  1. 红色 ❌ - 编写测试。运行测试。由于您尚未编写任何代码来通过测试,因此新测试失败。
  2. 绿色✔️ - 编写通过测试(以及所有之前的测试)的代码。不必耍小聪明,只要编写代码确保测试通过就行!
  3. 重构♻️ - 重构你的代码!重构的原因有很多,例如效率、代码风格和可读性。重构时,请确保你的代码仍然能够通过测试。

这个过程的美妙之处在于,只要您的测试代表了您的代码的用例,您现在开发的代码就会(a)不包含任何镀金,并且(b)将在您以后每次运行测试时进行测试。

我们的 TDD 候选:对象验证器

我们的 TDD 候选对象是一个对象验证函数。该函数将接受一个对象和一些条件作为输入。最初,我们的要求如下:

  • 验证器将接受两个参数:要验证的对象和标准对象
  • 验证器将返回一个具有布尔valid属性的对象,该属性指示该对象是否有效(true)或无效(false)。

稍后,我们将添加一些更复杂的标准。

设置我们的环境

对于本练习,让我们创建一个新目录并安装jest,这是我们将使用的测试框架。

mkdir object-validator
cd object-validator
yarn add jest@24.9.0
Enter fullscreen mode Exit fullscreen mode

注意:您专门安装 24.9.0 版本的 jest 是为了确保您的版本与我在本教程中使用的版本相匹配。

最后一条命令会package.json为我们创建一个文件。在这个文件中,让我们修改 scripts 部分,以便--watchAll在运行时使用标志运行 jest yarn test。这意味着当我们修改文件时,所有测试都会重新运行!

我们的package.json文件现在看起来应该是这样的:

{
  "scripts": {
    "test": "jest"
  },
  "dependencies": {
    "jest": "24.9.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

接下来,创建两个文件:validator.jsvalidator.test.js。前者将包含我们验证器的代码,后者将包含我们的测试。(默认情况下,jest 会在以 结尾的文件中搜索测试.test.js)。

创建空验证器并进行初始测试

在我们的文件中validator.js,让我们从简单的导出开始,null这样我们就有一些东西可以导入到我们的测试文件中。

验证器.js

module.exports = null;
Enter fullscreen mode Exit fullscreen mode

验证器.测试.js

const validator = require('./validator');
Enter fullscreen mode Exit fullscreen mode

初步测试

在初始测试中,我们将检查验证器是否在未提供任何条件的情况下判定对象有效。现在就来编写这个测试。

验证器.测试.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);
  });
});
Enter fullscreen mode Exit fullscreen mode

现在我们运行测试!注意,我们实际上还没有为这个validator函数编写任何代码,所以这个测试最好失败。

千万不要跳过这一步!跳过“红绿重构”循环中的“红”部分总是很诱人,但你应该先花时间让测试失败。这样你才能测试你的测试……换句话说,你需要确认测试在应该失败的时候失败,否则就不能正确地测试你的软件。

yarn test
Enter fullscreen mode Exit fullscreen mode

如果一切顺利,您应该会看到我们的测试失败了:

validator
  ✕ should return true for an object with no criteria (2ms)
Enter fullscreen mode Exit fullscreen mode

使测试通过

既然我们已经确认测试失败了,那就让它通过吧。为此,我们只需在validator.js文件里导出一个返回所需对象的函数即可。

验证器.js

const validator = () => {
  return { valid: true };
};

module.exports = validator;
Enter fullscreen mode Exit fullscreen mode

我们的测试应该仍在控制台中运行,所以如果我们看一下,我们应该会看到我们的测试现在正在通过!

validator
  ✓ should return true for an object with no criteria
Enter fullscreen mode Exit fullscreen mode

继续循环...

让我们再添加几个测试。我们知道,我们需要根据一些条件判断一个对象是否通过。现在,我们将添加两个测试来实现这一点。

验证器.测试.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);
});
Enter fullscreen mode Exit fullscreen mode

现在我们运行测试,确保两个新测试失败……但其中一个没有!这在 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
Enter fullscreen mode Exit fullscreen mode

obj现在,让我们传递这些测试。我们将更新验证函数,以返回传递的结果criteria

验证器.js

const validator = (obj, criteria) => {
  if (!criteria) {
    return { valid: true };
  }
  return { valid: criteria(obj) };
};

module.exports = validator;
Enter fullscreen mode Exit fullscreen mode

我们的测试全部通过了!现在我们应该考虑重构,但目前看来机会不多。我们继续创建测试。现在,我们要考虑到我们需要能够评估多个条件。

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);
});
Enter fullscreen mode Exit fullscreen mode

我们的两个新测试失败了,因为我们的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;
Enter fullscreen mode Exit fullscreen mode

新的测试通过了,但是之前的测试(这些测试被视为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);
});
Enter fullscreen mode Exit fullscreen mode

所有测试都通过了,代码又恢复正常了!这次,我觉得我们可以合理地重构代码了。我们回想起可以使用every数组方法,这很符合我们团队的风格。

验证器.js

const validator = (obj, criteria) => {
  if (!criteria) {
    return { valid: true };
  }
  const valid = criteria.every(criterion => criterion(obj));
  return { valid };
};

module.exports = validator;
Enter fullscreen mode Exit fullscreen mode

干净多了,测试也通过了。仔细看看,由于测试的全面,我们对重构的信心有多大!

处理相对较大的需求变更

我们对验证器的现状很满意,但用户测试表明,我们确实需要能够根据验证结果支持错误消息。此外,我们需要按字段名称聚合错误消息,以便能够在正确的输入字段旁边显示给用户。

我们决定我们的输出对象需要类似于以下形状:

{
  valid: false,
  errors: {
    username: ["Username must be at least 6 characters"],
    password: [
      "Password must be at least 6 characters",
      "Password must match password confirmation"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

让我们编写一些测试来适应新功能。我们很快意识到,它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'],
    },
  });
});
Enter fullscreen mode Exit fullscreen mode

现在我们运行测试,发现最后一个测试失败了。让我们让它通过。

验证器.测试.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;
Enter fullscreen mode Exit fullscreen mode

现在,第一个测试和最后一个测试通过了,但其他测试都失败了。这是因为我们改变了输入的形状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
Enter fullscreen mode Exit fullscreen mode

既然我们知道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'],
      },
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

如果我们检查测试,它们都通过了!

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
Enter fullscreen mode Exit fullscreen mode

看起来不错。现在我们来考虑一下如何重构。我当然不喜欢我们解决方案中的嵌套语句,而且当我们的代码仍然倾向于使用数组方法时,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;
Enter fullscreen mode Exit fullscreen mode

我们的测试仍然顺利通过,我们对重构后的代码非常满意validator!当然,我们可以而且应该继续构建测试用例,以确保能够处理多个字段以及每个字段的多个错误,但这个探索就留给你自己去探索吧!

结论

测试驱动开发使我们能够在实际编写代码之前定义代码所需的功能。它使我们能够系统地测试和编写代码,并让我们对重构充满信心。与任何方法一样,TDD并非完美无缺。如果您未能确保测试首先失败,则很容易出错。此外,如果您编写的测试不够彻底和严格,它可能会给您一种虚假的信心。


如果您学到了一些东西,请给这篇文章点赞💓、🦄或🔖!

鏂囩珷鏉簮锛�https://dev.to/nas5w/a-gentle-introduction-to-test-driven-development-creating-an-object-validator-3kda
PREV
使用 Typescript 在 React 中创建待办事项列表应用程序第 1 部分:引导程序和初始组件第 2 部分:添加动态行为第 3 部分:扩展功能以能够添加项目
NEXT
100+ 免费插画资源,助您完成个人或客户项目