[譯] 逆向工程,如何在 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索