[译] 逆向工程,如何在 JavaScript 中打造一个测试库

逆向工程,如何在 JavaScript 中打造一个测试库

我知道你在想什么,在已有那么多测试库的状况下再本身造轮子?其实不是,本文是关于如何可以逆向工程,以及理解背后发生的事。这么作的目的,是为了可以让你更普遍同时更深入地理解你所使用的库。前端

再强调一遍,我并不打算彻底实现一个测试库,只是粗略地看看有哪些公共 API,粗略地理解一下,而后实现它们。我但愿经过这种方式能够对整个架构有所了解,知道如何删除、扩展模块以及了解各个部分的难易程度。node

但愿你享受这个过程:)android

咱们将介绍如下内容:ios

  • 为何,试着解释逆向工程的全部好处
  • 是什么,咱们将构建什么,不构建什么
  • 构建,手把手教你构建过程

为何

不少年前,在我做为一位软件开发人员的职业生涯开始的时候,我询问过一些高级开发人员他们如何进步。其中一个突出的答案是逆向工程,或者更确切地说是重建他们正在使用或者感兴趣的框架或库。git

对我来讲听起来像是在试图从新造轮子。有什么好处,难道咱们没有足够的库来作一样的事情吗?github

固然,这个论点是有道理的。不要由于不喜欢库的某些地方就从新造轮子,除非你真的须要,但有时候你确实须要从新造轮子。npm

何时?后端

当你想要在你的职业中变得更好的时候。架构

听起来很模糊app

确实,毕竟有不少方法可让你变得更好。我认为要真正理解某些东西仅仅使用它是不够的 —— 你须要构建它。

什么?所有吗?

取决于库或框架的大小。有些库足够小,值得从头构建,但大多数都不是。尝试实现某些东西有着不少价值,仅仅是开始就能让你明白许多(若是卡住了)。这就是练习的目的,试着理解更多。

是什么

咱们在开头提到了构建一个测试库,具体是哪一个测试库呢?咱们来看下大部分 JavaScript 里的测试库长什么样子:

describe('suite', () => {
  it('should be true', () => {
    expect(2 > 1).toBe(true)
  })
})
复制代码

这就是咱们将要构建的东西 —— 让上述代码成功运行而且在构建过程当中评论架构好坏,有可能的话,放进一个库里使其美观:)

让咱们开始吧。

构建

If you build it they will come(只要付出就会有回报)

真的吗?

你知道电影《梦幻之地(Field of Dreams)》吗?

别啰嗦快开始吧

Expect,断言咱们的值

让咱们从最基础的声明开始 —— expect() 函数。经过调用方式咱们能够看出不少:

expect(2 > 1).toBe(true)
复制代码

expect() 看起来像是接收一个 boolean 做为参数的函数,它返回一个对象,对象有一个 toBe() 方法,这样就能够将 expect() 的值以及传递给 toBe() 函数的值进行比较。让咱们试着去写出大概:

function expect(actual) {
  return {
    toBe(expected) {
      if(actual === expected){
        /* do something*/
      } else {
        /* do something else*/
      }
    }
  }
}
复制代码

另外,若是匹配成功或者失败,咱们应该反馈一些声明。所以须要更多代码:

function expect(actual) {
  return {
    toBe(expected) {
      if(expected === actual){
        console.log(`Succeeded`)
      } else {
        console.log(`Fail - Actual: ${val}, Expected: ${expected}`)
      }
    }
  }
}

expect(true).toBe(true) // Succeeded
expect(3).toBe(2)  // Fail - Actual: 3, Expected: 2
复制代码

注意 else 的声明有一些更专业的信息并给咱们提供失败提示。

相似这样比较两个值的函数例如 toBe() 被称为 matcher。让咱们尝试添加另外一个 matcher toBeTruthy()truthy 匹配 JavaScript 中的不少值,这样咱们能够不用 toBe() 去匹配全部东西。

因此咱们在偷懒?

对的,这是最佳的理由:)

在 JavaScript 中,任何被认为是 truthy 的值都能成功执行,其它都会失败。让咱们去 MDN 看看那些值被认为是 truthy 的:

if (true)
if ({})
if ([])
if (42)
if ("0")
if ("false")
if (new Date())
if (-42)
if (12n)
if (3.14)
if (-3.14)
if (Infinity)
if (-Infinity)
复制代码

因此全部在 if 中执行后执行为 true 的都为 truthy。是时候添加上述方法了:

function expect(actual) {
  return {
    toBe(expected) {
      if(expected === actual){
        console.log(`Succeeded`)
      } else {
        console.log(`Fail - Actual: ${val}, Expected: ${expected}`)
      }
    },
    toBeTruthy() {
      if(actual) {
        console.log(`Succeeded`)
      } else {
        console.log(`Fail - Expected value to be truthy but got ${actual}`)
      }
    }
  }
}

expect(true).toBe(true) // Succeeded
expect(3).toBe(2)  // Fail - Actual: 3, Expected: 2
expect('abc').toBeTruthy();
复制代码

我不知道你的意见,可是我以为 expect() 方法开始变得臃肿了。让咱们把咱们的 matchers 放进一个 Matchers 类里:

class Matchers {
  constructor(actual) {
    this.actual = actual;
  }

  toBe(expected) {
    if(expected === this.actual){
      console.log(`Succeeded`)
    } else {
      console.log(`Fail - Actual: ${this.actual}, Expected: ${expected}`)
    }
  }

  toBeTruthy() {
    if(this.actual) {
      console.log(`Succeeded`)
    } else {
      console.log(`Fail - Expected value to be truthy but got ${this.actual}`)
    }
  }
}

function expect(actual) {
  return new Matchers(actual);
}
复制代码

it,咱们的测试方法

在咱们的眼里它应该是这样运行的:

it('test method', () => {
  expect(3).toBe(2)
})
复制代码

好的,将这点东西逆向工程咱们差很少能写出咱们本身的 it() 方法:

function it(testName, fn) {
  console.log(`test: ${testName}`);
  fn();
}
复制代码

让咱们停下来思考一下。咱们想要什么样的行为?我看到过一旦出现故障就退出运行的单元测试库。我想若是你有 200 个单元测试(并不是说你应该在一个文件里放 200 个测试),咱们也绝对不想等待它们完成,最好直接告诉我哪里报错,好让我能够解决它。为了实现后者,咱们须要稍微调整咱们的 matchers:

class Matchers {
  constructor(actual) {
    this.actual = actual;
  }

  toBe(expected) {
    if(expected === actual){
      console.log(`Succeeded`)
    } else {
      throw new Error(`Fail - Actual: ${val}, Expected: ${expected}`)
    }
  }

  toBeTruthy() {
    if(actual) {
      console.log(`Succeeded`)
    } else {
      console.log(`Fail - Expected value to be truthy but got ${actual}`)
      throw new Error(`Fail - Expected value to be truthy but got ${actual}`)
    }
  }
}
复制代码

这意味着咱们的 it() 函数须要捕获全部错误:

function it(testName, fn) {
  console.log(`test: ${testName}`);
  try {
    fn();
  } catch(err) {
    console.log(err);
    throw new Error('test run failed');
  }

}
复制代码

如上所示,咱们不只捕获了错误并打印日志,咱们还从新抛出错误以终止运行。再次,这样作的主要缘由是咱们认为报了错就没有必要继续测试了。你能够以合适的方式实现这个功能。

Describe,咱们的测试套件

如今,咱们介绍了如何编写 it()expect(),甚至还介绍了几个 matcher 方法。可是,全部测试库都应该具备套件概念,这表示这是一组测试。

让咱们看看代码是什么样的:

describe('our suite', () => {
  it('should fail 2 != 1', () => {
    expect(2).toBe(1);
  })

  it('should succeed', () => { // 技术上讲它不会运行到这里,在第一个测试后它将崩溃
    expect('abc').toBeTruthy();
  })
})
复制代码

至于实现,咱们知道失败的测试会引起错误,所以咱们须要捕获它以免整个程序崩溃:

function describe(suiteName, fn) {
  try {
    console.log(`suite: ${suiteName}`);
    fn();
  } catch(err) {
    console.log(err.message);
  }
}
复制代码

运行代码

此时咱们的完整代码应该以下所示:

// app.js

class Matchers {
  constructor(actual) {
    this.actual = actual;
  }

  toBe(expected) {
    if (expected === this.actual) {
      console.log(`Succeeded`)
    } else {
      throw new Error(`Fail - Actual: ${this.actual}, Expected: ${expected}`)
    }
  }

  toBeTruthy() {
    if (actual) {
      console.log(`Succeeded`)
    } else {
      console.log(`Fail - Expected value to be truthy but got ${this.actual}`)
      throw new Error(`Fail - Expected value to be truthy but got ${this.actual}`)
    }
  }
}

function expect(actual) {
  return new Matchers(actual);
}

function describe(suiteName, fn) {
  try {
    console.log(`suite: ${suiteName}`);
    fn();
  } catch(err) {
    console.log(err.message);
  }
}

function it(testName, fn) {
  console.log(`test: ${testName}`);
  try {
    fn();
  } catch (err) {
    console.log(err);
    throw new Error('test run failed');
  }
}

describe('a suite', () => {
  it('a test that will fail', () => {
    expect(true).toBe(false);
  })

  it('a test that will never run', () => {
    expect(1).toBe(1);
  })
})

describe('another suite', () => {
  it('should succeed, true === true', () => {
    expect(true).toBe(true);
  })

  it('should succeed, 1 === 1', () => {
    expect(1).toBe(1);
  })
})
复制代码

当咱们在终端运行 node app.js 时,控制台应该长这样:

美化日志

如上所示,咱们的代码看起来正常运行,可是它看起来丑了。咱们能够作什么呢?颜色,丰富的色彩将让它看起来好点。使用库 chalk 咱们能够给日志注入生命:

npm install chalk --save
复制代码

接下来让咱们添加一些颜色、一些标签和空格,咱们的代码应以下所示:

const chalk = require('chalk');

class Matchers {
  constructor(actual) {
    this.actual = actual;
  }

  toBe(expected) {
    if (expected === this.actual) {
      console.log(chalk.greenBright(` Succeeded`))
    } else {
      throw new Error(`Fail - Actual: ${this.actual}, Expected: ${expected}`)
    }
  }

  toBeTruthy() {
    if (actual) {
      console.log(chalk.greenBright(` Succeeded`))
    } else {
      throw new Error(`Fail - Expected value to be truthy but got ${this.actual}`)
    }
  }
}

function expect(actual) {
  return new Matchers(actual);
}

function describe(suiteName, fn) {
  try {
    console.log('\n');
    console.log(`suite: ${chalk.green(suiteName)}`);
    fn();
  } catch (err) {
    console.log(chalk.redBright(`[${err.message.toUpperCase()}]`));
  }
}

function it(testName, fn) {
  console.log(` test: ${chalk.yellow(testName)}`);
  try {
    fn();
  } catch (err) {
    console.log(` ${chalk.redBright(err)}`);
    throw new Error('test run failed');
  }
}

describe('a suite', () => {
  it('a test that will fail', () => {
    expect(true).toBe(false);
  })

  it('a test that will never run', () => {
    expect(1).toBe(1);
  })
})

describe('another suite', () => {
  it('should succeed, true === true', () => {
    expect(true).toBe(true);
  })

  it('should succeed, 1 === 1', () => {
    expect(1).toBe(1);
  })
})
复制代码

运行以后,控制台应该以下所示:

总结

咱们的目标是逆向工程一个至关小的库,如单元测试库。经过查看代码,咱们能够推断它背后的内容。

咱们创造了一些东西,一个起点。话虽如此,咱们须要意识到大多数单元测试库都带有不少其余东西,例如,处理异步测试、多个测试套件、模拟数据、窥探更多的 matcher 等等。经过尝试理解你天天使用的内容能够得到不少东西,但也请你意识到你没必要彻底从新发明它以得到大量洞察力。

我但愿你可使用此代码做为起点,使用它、从头开始或扩展,决定权在你。

另外一个可能结果是你已经足够了解 OSS 并改进其中一个现有库。

记住,只要付出就有回报:

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索