[譯] 完美的 Javascript 單元測試

今天咱們討論的是如何編寫完美的單元測試以及如何確保測試的可讀性,可用性和可維護性。javascript

我發現有一個共性,那些告訴我單元測試沒用的人,一般都在編寫糟糕的單元測試。特別對於那些剛剛接觸單元測試的新手,這徹底能夠理解。寫出很棒的單元測試很難,它須要你常常練習才能夠。咱們今天要討論的全部事情都是經過很艱難的方式學到的; 不良單元測試的痛苦導致我爲如何編寫好的單元測試建立了本身的規則。咱們今天要討論的就是這些規則。前端

爲何糟糕的測試很是致命?

若是你拿到的程序代碼很混亂,那麼就會很難處理。但萬幸的是你能夠爲你的代碼寫一些單元測試,它們能夠幫到你。若是能有測試支持你,那麼處理那種混亂的代碼還 OK。測試能夠幫助你消除不良代碼的影響。java

可是不會有任何代碼能夠幫你處理一個糟糕的測試。你不能爲你的測試再去編寫測試。你也能夠,可是接下來你就必須爲你的測試的測試編寫測試,一個無窮無盡的循環,沒有人想要這樣……android

不良測試的特色

很難定義一組不良測試的特徵,由於不良測試不符合咱們即將要討論的任何規則。ios

若是你曾經看過一個測試而且不知道它正在測試什麼,或者你沒法明顯地識別它的斷言,那就是一個糟糕的測試。若是一個測試的描述寫的很差(我最喜歡用 it('works')),那它就是一個糟糕的測試。git

若是你發現測試沒有用,那麼它也是一個糟糕的測試。測試的所有目的是提升你的生產力、工做流程和對代碼庫的信心。若是測試作不到這些(或者讓它變得更糟),那麼它確定是一個糟糕的測試。github

我堅信糟糕的測試比沒有測試更糟糕後端

好的測試都有一個好名字

好消息是,一旦你習慣了測試,那些好的測試規則很容易記住,並且很是直觀!bash

一個好的測試有一個簡潔、描述性的名稱。若是你不能想出一個簡短的名字,那麼最好選擇清晰明確的名稱而不是僅僅省下了每行的長度。函數

it('filters products based on the query-string filters', () => {})
複製代碼

你應該可以從描述中瞭解到測試的目的是什麼。你有時會看到下面這種寫法,基於要測試的方法名稱給 it 測試命名:

it('#filterProductsByQueryString', () => {})
複製代碼

但這並無幫助 —— 想象一下若是你剛剛接觸這些代碼,你還得費力找出這個函數到底有什麼功能。在這種狀況下,方法名稱是很是具備描述性的,可是一個真正人類可讀的字符串老是更好,前提是你能想出來一個。

爲測試命名的另外一個指導方針是:確保你能夠在 it 開頭讀取到句子。因此,若是我正在閱讀下面的測試,我會讀到一句話:

「it filters products based on the query-string filters(它基於查詢字符串過濾器過濾產品)」

it('filters products based on the query-string filters', () => {})
複製代碼

看看下面這個描述,即便這個描述很是有描述性,可是測試並非用來執行這個操做的,因此這個描述會感受很是彆扭:

it('the query-string is used to filter products', () => {})
複製代碼

完美測試的三個步驟

當咱們爲測試起好了名字,咱們就該開始關注測試主體了。一個好的測試每次都遵循相同的模式:

it('filters products based on the query-string filters', () => {
  // 第一步:初始化
  // 第二步:調用代碼
  // 第三步:斷言
})
複製代碼

讓咱們依次看看這些步驟。

初始化

任何單元測試的第一個階段都是初始化:這是你按順序獲取測試數據的地方,也是模擬運行此測試可能須要的任何功能的地方。

it('filters products based on the query-string filters', () => {
  // 第一步:初始化
  const queryString = '?brand=Nike&size=M'

  const products = [
    { brand: 'Nike', size: 'L', type: 'sweater' },
    { brand: 'Adidas', size: 'M', type: 'tracksuit' },
    { brand: 'Nike', size: 'M', type: 't-shirt' },
  ]

  // 第二步:調用代碼
  // 第三步:斷言
})
複製代碼

初始化這步應該構建執行測試所需的一切。在上面的例子中,我建立了查詢字符串和我將用於測試的產品列表。注意我爲產品列表挑選的測試數據:我有一些故意與查詢字符串不匹配的數據,以及一個徹底匹配的數據。若是我只有與查詢字符串匹配的數據,那麼這個測試不能證實過濾是有效的。

調用代碼

這步一般是最短的:你應該在這裏調用須要測試的函數。第一步中你應該已經構造了測試數據,因此在這裏你能夠直接將它們做爲變量傳遞給函數。

it('filters products based on the query-string filters', () => {
  // 第一步:初始化
  const queryString = '?brand=Nike&size=M'

  const products = [
    { brand: 'Nike', size: 'L', type: 'sweater' },
    { brand: 'Adidas', size: 'M', type: 'tracksuit' },
    { brand: 'Nike', size: 'M', type: 't-shirt' },
  ]

  // 第二步:調用代碼
  const result = filterProductsByQueryString(products, queryString)

  // 第三步:斷言
})
複製代碼

若是測試數據很是少,我可能會合並第一步和第二步,但大部分時間我都發現將它們明確地按步驟拆分很是有價值,值得多寫幾行。

斷言

這是最好的一步!是你全部的努力獲得回報的地方,咱們在這裏檢查事情有沒有按照咱們指望的進行。

我稱之爲斷言步驟,由於咱們在這裏作一些斷言,可是如今我傾向於使用 Jest 和它的 expect 函數,因此若是你願意的話你也能夠稱之爲「指望步驟」。

it('filters products based on the query-string filters', () => {
  // 第一步:初始化
  const queryString = '?brand=Nike&size=M'

  const products = [
    { brand: 'Nike', size: 'L', type: 'sweater' },
    { brand: 'Adidas', size: 'M', type: 'tracksuit' },
    { brand: 'Nike', size: 'M', type: 't-shirt' },
  ]

  // 第二步:調用代碼
  const result = filterProductsByQueryString(products, queryString)

  // 第三步:斷言
  expect(result).toEqual([{ brand: 'Nike', size: 'M', type: 't-shirt' }])
})
複製代碼

通過上面這些操做,如今咱們有了一個完美的單元測試:

  1. 它有一個描述性的名稱,讀起來很是清楚,簡潔。
  2. 它有一個明確的初始化階段,咱們在這裏構建測試數據。
  3. 調用步驟僅限於調用咱們的函數並使用咱們的測試數據。
  4. 咱們的斷言很是明確,清楚地驗證了咱們正在測試的功能。

小改進

雖然實際上我不會在實際測試中包含 // STEP ONE: SETUP 這些註釋,可是我發如今三個部分之間加上一個空行很是有用。因此,若是這個測試真的出如今個人代碼庫中,那麼它應該是下面這樣:

it('filters products based on the query-string filters', () => {
  const queryString = '?brand=Nike&size=M'
  const products = [
    { brand: 'Nike', size: 'L', type: 'sweater' },
    { brand: 'Adidas', size: 'M', type: 'tracksuit' },
    { brand: 'Nike', size: 'M', type: 't-shirt' },
  ]

  const result = filterProductsByQueryString(products, queryString)

  expect(result).toEqual([{ brand: 'Nike', size: 'M', type: 't-shirt' }])
})
複製代碼

若是咱們正在構建一個包含產品的系統,我但願建立一種更簡單的方法來建立這些產品。因此我構建了 test-data-bot 庫,它能夠輕鬆作到上面的事情。我不會深刻介紹它的工做原理,但它可讓你輕鬆地建立**工廠模式(factories)**來構建測試數據。若是咱們用了這個構建工具(README 有詳細的說明),咱們能夠像下面這樣重寫測試:

it('filters products based on the query-string filters', () => {
  const queryString = '?brand=Nike&size=M'
  const productThatMatches = productFactory({ brand: 'Nike', size: 'M' })

  const products = [
    productFactory({ brand: 'Nike', size: 'L' }),
    productFactory({ brand: 'Adidas', size: 'M' }),
    productThatMatches,
  ]

  const result = filterProductsByQueryString(products, queryString)

  expect(result).toEqual([productThatMatches])
})
複製代碼

經過這樣作,咱們移除了全部與測試無關的產品的細節(注意 type 字段如今並不在咱們的測試中),而後經過更新工廠,咱們能夠輕鬆地讓測試數據和真實數據保持同步。

我還爲我想要匹配的產品建立了一個常量,這樣咱們就能夠在斷言步驟中複用它。避免了重複的代碼並使測試更加清晰 —— 命名爲 productThatMatches 的測試數據自己就是一個強烈的暗示,告訴咱們這就是指望函數返回的內容。

總結

若是你在編寫測試的時候遵循了上面的規則,我相信你必定會發現你的測試更容易使用,並且對你的開發流程更有幫助。測試和其它任何事情同樣:須要時間和練習。記住三個步驟:setupinvokeassert,你必定能夠寫出完美的單元測試😼。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索