- 原文地址:Reverse Engineering, how YOU can build a testing library in JavaScript
- 原文做者:Chris Noring
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:DEARPORK
- 校对者:三月源, yzw7489757
我知道你在想什么,在已有那么多测试库的状况下再本身造轮子?其实不是,本文是关于如何可以逆向工程,以及理解背后发生的事。这么作的目的,是为了可以让你更普遍同时更深入地理解你所使用的库。前端
再强调一遍,我并不打算彻底实现一个测试库,只是粗略地看看有哪些公共 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(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('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');
}
}
复制代码
如上所示,咱们不只捕获了错误并打印日志,咱们还从新抛出错误以终止运行。再次,这样作的主要缘由是咱们认为报了错就没有必要继续测试了。你能够以合适的方式实现这个功能。
如今,咱们介绍了如何编写 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 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。