• 赚钱入口【需求资源】限时招募流量主、渠道主,站长合作;【合作模式】CPS长期分成,一次推广永久有收益。主动打款,不扣量;

通过测试重构JavaScript

JavaScript rin, seun 1年前 (2020-06-22) 224次浏览 0个评论

现在我们已经测试了涵盖代码行为的测试,我们准备进行一些重构并改进代码以使其更易于使用。

手头的代码为与书关联的媒体生成文件名。这是测试记忆的一种方法:

const fileName = Publisher.generateFilename({
  publishOn: new Date(2021, 3, 1),
  categoryPrefix: 'tech',
  kind: 'software-design',
  id: 123,
  title: 'Software Design',
})
expect(fileName).toMatch(/2021-4techsoftware-design123[0-9]{5}-softwared\.jpg/)

促使这种重构的原因是,我们的经理要求我们对此输出进行更改。文件名中的每个单独部分都应用破折号(-)分隔。在上面的输出中,您可以看到这在输出的不同部分不一致地发生。现在,将所有字符串连接起来并添加破折号是一项非常手动的工作。让我们看看是否可以将我们的工作分为两个单独的步骤:

  1. 进行工作以使更改变得容易(请注意:这可能很难)。
  2. 轻松进行更改。

这是一个普遍的误解,您需要花费大量时间来重构代码。相反,请尝试将重构视为要使新功能更容易,更快地实现的工作。这也更容易传达给利益相关者!

使更改变得容易

如果我们将文件名视为一系列部分,则可以开始取得进展。我们知道我们已经通过测试来确认一切正常,并且我们现在的目标是进行一系列更改以改进代码。我们的步骤应该很小,并且每次更改后都应该运行测试。我们想尽快知道是否有任何损坏!

您是否曾经破坏过您的应用程序并疯狂地开始撤消某些事情,以尝试恢复到其正常工作的状态?还是在重构过程中途遇到了测试失败的情况?尝试养成在每次更改进行一些小的更改并运行测试的习惯,以帮助您在出现问题时立即意识到任何问题。立即撤消比在更改中回滚要容易得多。

class Publisher {
  static generateFilename(target) {
    let fileName = `${target.publishOn.getFullYear()}-${
      target.publishOn.getMonth() + 1
    }`
    // more code here
  }
}

我要做的第一个更改是将文件名生成的每个部分拆分为自己的函数。让我们开始学习第一部分并将其放入函数中:

const publishDatePart = (target) => {
  return `${target.publishOn.getFullYear()}-${target.publishOn.getMonth() + 1}`

然后调用它:

class Publisher {
  static generateFilename(target) {
    let fileName = publishDatePart(target)
    // more code here
  }
}

运行测试确认我们没有破坏任何东西。另一个好的指导原则是,在重构时,您应该能够停止并将代码保留在比发现代码更好的地方。尽管这只是一小步,但现在它已经分开了一些,因此更容易弄清楚并处理此代码,因此我们进行了改进。

退出所有功能

我将为您保留每个函数的详细信息,但这是我们在多次执行上述步骤后剩下的:

class Publisher {
  static generateFilename(target) {
    let fileName = publishDatePart(target)

    fileName += target.categoryPrefix
    fileName += kindPart(target)

    fileName += String(target.id)
    fileName += randomPart()
    fileName += target.isPersonal ? target.ageRange : ''

    fileName += titlePart(target)
    fileName += '.jpg'

    return fileName
  }
}

const titlePart = (target) => {
  let truncatedTitle = target.title.replace(/[^\[a-z\]]/gi, '').toLowerCase()
  let truncateTo = truncatedTitle.length > 9 ? 9 : truncatedTitle.length
  return `-${truncatedTitle.slice(0, truncateTo)}`
}

const randomPart = () => {
  return Array.from({ length: 5 }, (_) => Math.floor(Math.random() * 10)).join(
    ''
  )
}
const kindPart = (target) => {
  return target.kind.replace('_', '')
}

const publishDatePart = (target) => {
  return `${target.publishOn.getFullYear()}-${target.publishOn.getMonth() + 1}`
}

在这部分工作中,抵制更改任何代码的冲动确实很重要。函数的主体与以前完全相同。我刚刚将它们提取到函数中。其中有些我们今天甚至可能无法重构。但这没关系,我们仍在取得长足进步,并且下次我们开始使用该代码时,代码将更加易于使用。更重要的是,我们现在准备更改功能!

进行功能更改

我喜欢被测试驱动,所以知道我们的输出中的破折号会比当前更多,所以让我们经历每个测试并对其进行更新,以便它们在我们期望的位置出现破折号。这是一个例子:

it('removes other special characters from the book title', () => {
  const fileName = Publisher.generateFilename({
    publishOn: new Date(2021, 3, 1),
    categoryPrefix: 'bio',
    kind: 'biography',
    id: 123,
    title: '(My) <title$>',
  })
  expect(fileName).toMatch(/2021-4-bio-biography-123-[0-9]{5}-mytitle\.jpg/)
})

如果我们现在运行测试,那么所有七个都将失败!让我们看看是否可以让他们回到过去。如果您觉得不知所措,通常我只会选择一个测试(在Jest中,您可以将itto 更改为it.only并且仅运行该测试)。这样,您就不会有很大的输出,一旦您通过了一项测试,就可以运行其余的。

我们要做的第一件事是遍历每个单独的部分,并删除当前输出的所有破折号。这样我们就可以使它们统一-没有单独的部分负责添加破折号。然后,我们可以轻松地制作出所有零件,并将它们与破折号组合起来。碰巧的是,我们只需要对这样做titlePart,就可以丢失字符串插值,仅返回标题部分:

const titlePart = (target) => {
  let truncatedTitle = target.title.replace(/[^\[a-z\]]/gi, '').toLowerCase()
  let truncateTo = truncatedTitle.length > 9 ? 9 : truncatedTitle.length
  return truncatedTitle.slice(0, truncateTo)
}

现在,我们可以轻松进行更改,让我们为本书的所有部分创建一个数组,然后将它们与连字符一起用短划线连起来:

class Publisher {
  static generateFilename(target) {
    const parts = [
      publishDatePart(target),
      target.categoryPrefix,
      kindPart(target),
      String(target.id),
      randomPart(),
      target.isPersonal ? target.ageRange : '',
      titlePart(target),
    ].filter(Boolean)

    const extension = '.jpg'
    return parts.join('-') + extension
  }
}

有一个轻微的“陷阱”,如果不过滤filter(Boolean),我们将包含空字符串应该target.isPersonalfalse,这意味着我们最终将空字符串用破折号连接起来并获得了双精度。但是一旦发现这一点,我们就会再次活跃起来,并实现我们的功能。

结论

我们可以在这里做更多的事情。该代码绝不是完美的。但是它比以前干净得多,它具有全面的测试套件,并且通过将其功能扩展到较小的方法中,我们为下一步需要添加功能的代码进一步迭代奠定了基础。花费在编写测试上的额外时间现在已经得到回报,并且只要我们重新访问代码库的这一部分,它将继续一次又一次地得到回报。

喜欢 (0)

您必须 登录 才能发表评论!