- 原文地址: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、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。