用 Jest 進行 JavaScript 測試(2019)

做者:Valentino Gagliardi

翻譯:瘋狂的技術宅javascript

原文:https://www.valentinog.com/bl...html

未經容許嚴禁轉載前端

測試是什麼意思?

在技​​術術語中測試意味着檢查咱們的代碼是否符合某些預期。例如:給定一些輸入,一個名爲「transformer」的函數應返回預期的輸出java

有許多類型的測試,很快你就會被術語所淹沒,讓咱們長話短書。測試分爲三大類react

  • 單元測試
  • 集成測試
  • UI測試

在這個 Jest 教程中,咱們將僅涵蓋單元測試,但在文章的最後,你將找到更多用於其餘類型測試的資源。git

什麼是Jest?

Jest 是一個 JavaScript 測試運行器,即用於建立、運行結構化測試的 JavaScript 庫。 Jest 做爲 NPM 包發佈,你能夠將其安裝在任何 JavaScript 項目中。 Jest 是目前最受歡迎的測試運行器之一,也是 Create React App 的默認選擇。程序員

首先要作的事情:我怎麼知道要測試些什麼?

當談到測試時,即便是簡單的代碼塊也會使初學者癱瘓。最多見的問題是「我怎麼知道要測試些什麼?」。若是你正在編寫 Web 應用,那麼一個好的起點就是測試應用的每一個頁面和每一個用戶交互。但 Web 應用也由單元代碼組成,如函數和模塊,也須要進行測試。不少時候有兩種狀況:github

  • 你維護沒有測試祖傳代碼
  • 你必須憑空實現新功能

該怎麼辦?對於這兩種狀況,你能夠經過考慮代碼來檢查,以檢查給定函數是否產生預期結果**。如下是典型測試流程的樣子:面試

應該怎麼辦? 對於這兩種狀況,你能夠經過將測試看做檢查給定函數是否產生預期結果的代碼來幫助本身。 如下是典型測試流程的樣子:正則表達式

  1. 導入要測試的函數
  2. 給函數輸入
  3. 定義指望輸出
  4. 檢查函數是否按照預期輸出

就是這樣。若是你按照這些術語思考,測試再也不可怕:輸入 - 預期輸出 - 斷言結果。接下來咱們還會看到一個方便的工具,用於檢查幾乎確切的測試內容。如今就動手學習 Jest!

設置項目

與每一個 JavaScript 項目同樣,你須要一個 NPM 環境(確保在你的系統上安裝了 Node)。建立一個新文件夾並用如下命令初始化項目:

mkdir getting-started-with-jest && cd $_
npm init -y

接下來安裝Jest:

npm i jest --save-dev

咱們還須要用配置一個 NPM 腳本,用於從命令行運行咱們的測試。打開 package.json 並配置名爲「test」的腳本以運行Jest:

"scripts": {
    "test": "jest"
  },

規範和測試驅動開發

做爲開發者,咱們都喜歡創意自由。可是當談到嚴肅的事情時,大部分時間你都沒有那麼多的特權。一般咱們必須遵循規範,即創建的書面或口頭描述

在本教程中,咱們從項目經理那裏獲得了一個至關簡單的規範。一個超級重要的客戶端須要一個函數來過濾一個對象數組。

對於每一個對象,咱們必須檢查名爲「url」的屬性,若是屬性的值與給定的術語匹配,那麼咱們應該在結果數組中包含匹配的對象。做爲一個精通測試的 JavaScript 開發人員,你想要遵循測試驅動開發,這是一個強制在開始編碼以前編寫失敗測試的學科。

默認狀況下,Jest 但願在項目下名爲 tests 的文件夾中找到測試文件。建立新文件夾:

cd getting-started-with-jest
mkdir __tests__

接下來在 tests 中建立一個名爲 filterByTerm.spec.js 的新文件。你可能想知道爲何擴展名是「.spec。」。這是一個借用 Ruby 的約定,用於將文件標記爲給定功能的規範

如今來測試吧!

測試結構和第一次失敗的測試

如今建立你的第一次Jest測試。打開 filterByTerm.spec.js 並建立一個測試塊:

describe("Filter function", () => {
  // test stuff
});

咱們的第一個朋友是 describe,一個用於包含一個或多個相關測試的 Jest 方法。每次開始爲功能編寫一套新測試時,都會將其包含在 describe 塊中。正如你所看到的,它須要兩個參數:一個用於描述測試套件的字符串,還有一個用於包裝實際測試的回調函數。

接下來咱們將遇到另外一個名爲 test 的函數,它是實際的測試塊:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    // actual test
  });
});

這時咱們已準備好編寫測試了。請記住,測試是關於輸入、功能和預期輸出的問題。首先定義一個簡單的輸入,一個對象數組:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
  });
});

接下來定義預期結果。根據規範,測試中的函數應該省略其 url 屬性與給定搜索項不匹配的對象。咱們能夠期待例如具備單個對象的數組,給定 「link」 做爲搜索項:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
  });
});

如今準備編寫實際的測試。咱們將使用 expect 和一個 Jest matcher 來檢查這個函數在調用時返回的預期結果。這是測試:

expect(filterByTerm(input, "link")).toEqual(output);

爲了進一步細分,你能夠在代碼中調用函數:

filterByTerm(inputArr, "link");

在 Jest 測試中,你應該將函數調用包含在 expect 中,它與匹配器(用於檢查輸出的Jest函數)一塊兒進行實際測試。這是完整的測試:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
    expect(filterByTerm(input, "link")).toEqual(output);
  });
});

(有關 Jest 匹配器的更多信息,請查看文檔(https://jestjs.io/docs/en/get...))。

你能夠這樣執行測試:

npm test

你會看到測試失敗了:

FAIL  __tests__/filterByTerm.spec.js
  Filter function
    ✕ it should filter by a search term (2ms)
  ● Filter function › it should filter by a search term (link)
    ReferenceError: filterByTerm is not defined
       9 |     const output = [{ id: 3, url: "https://www.link3.dev" }];
      10 | 
    > 11 |     expect(filterByTerm(input, "link")).toEqual(output);
         |     ^
      12 |   });
      13 | });
      14 |

「ReferenceError: filterByTerm is not defined」. 實際上這是一件好事。咱們會在下一節修復它!

修復測試

真正缺乏的是 filterByTerm 的實現。爲方便起見,咱們將在測試所在的同一文件中建立該函數。在一個實際項目中,你須要在另外一個文件中定義該函數並從測試文件中導入它

爲了進行測試,咱們將使用一個名爲 filter 的原生 JavaScript 函數,它能夠過濾掉數組中的元素。這是 filterByTerm 的最小實現:

function filterByTerm(inputArr, searchTerm) {
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(searchTerm);
  });
}

如下是它的工做原理:對於輸入數組的每一個元素,咱們檢查「url」屬性,使用 match 方法將其與正則表達式進行匹配。這是完整的代碼:

function filterByTerm(inputArr, searchTerm) {
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(searchTerm);
  });
}
describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
    expect(filterByTerm(input, "link")).toEqual(output);
  });
});

再次運行測試:

npm test

看到它經過了!

PASS  __tests__/filterByTerm.spec.js
  Filter function
    ✓ it should filter by a search term (link) (4ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.836s, estimated 1s

很好。但咱們完成了測試嗎?尚未。 使咱們的函數失敗須要什麼條件?讓咱們用大寫搜索詞強調函數:

function filterByTerm(inputArr, searchTerm) {
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(searchTerm);
  });
}
describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
    expect(filterByTerm(input, "link")).toEqual(output);
    expect(filterByTerm(input, "LINK")).toEqual(output); // New test
  });
});

運行測試......它將失敗。該再次修復它了!

Jest Tutorial: fixing the test for uppercase

Jest Tutorial:修復大寫測試

filterByTerm 也應該考慮大寫的搜索術語。換句話說,即便搜索項是大寫字符串,它也應該返回匹配的對象:

filterByTerm(inputArr, "link");
filterByTerm(inputArr, "LINK");

爲了測試這種狀況,咱們引入了一個新測試:

expect(filterByTerm(input, "LINK")).toEqual(output); // New test

爲了使它經過,咱們能夠調整提供給 match 的正則表達式:

//
    return arrayElement.url.match(searchTerm);
//

咱們能夠構建一個不區分大小寫的正則表達式,而不是直接傳遞 searchTerm,也就是說,不管怎樣的字符串都匹配的表達式。這是修復:

function filterByTerm(inputArr, searchTerm) {
  const regex = new RegExp(searchTerm, "i");
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

這是完整的測試:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id: 1, url: "https://www.url1.dev" },
      { id: 2, url: "https://www.url2.dev" },
      { id: 3, url: "https://www.link3.dev" }
    ];
    const output = [{ id: 3, url: "https://www.link3.dev" }];
    expect(filterByTerm(input, "link")).toEqual(output);
    expect(filterByTerm(input, "LINK")).toEqual(output);
  });
});
function filterByTerm(inputArr, searchTerm) {
  const regex = new RegExp(searchTerm, "i");
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}

再次運行並看到它經過。作得好!做爲練習,你要寫兩個新的測試並檢查如下條件:

  1. 測試搜索詞「uRl」
  2. 測試空搜索詞。該函數應如何處理?

你將如何構建這些新測試?

在下一節中,咱們將看到測試的另外一個重要主題:代碼覆蓋率

代碼覆蓋率

什麼是代碼覆蓋率?在談論它以前,先讓咱們快速調整一下代碼。在項目根目錄中建立一個名爲 src 的新文件夾,並建立一個名爲 filterByTerm.js 的文件,放置並導出咱們的函數:

mkdir src && cd _$
touch filterByTerm.js

這是文件 filterByTerm.js

function filterByTerm(inputArr, searchTerm) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  const regex = new RegExp(searchTerm, "i");
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}
module.exports = filterByTerm;

如今僞裝我是你的新同事。我對測試一無所知,我應該直接在該函數內部添加一個新的 if語句,而不是要求更多的上下文:

function filterByTerm(inputArr, searchTerm) {
  if (!searchTerm) throw Error("searchTerm cannot be empty");
  if (!inputArr.length) throw Error("inputArr cannot be empty"); // new line
  const regex = new RegExp(searchTerm, "i");
  return inputArr.filter(function(arrayElement) {
    return arrayElement.url.match(regex);
  });
}
module.exports = filterByTerm;

filterByTerm 中有一行新的代碼,彷佛不會被測試。除非我告訴你「有一個新的測試聲明」你不會在咱們的函數中確切地知道測試。幾乎不可能想象咱們的代碼能夠採用的全部路徑,所以須要一種有助於揭示這些盲點的工具

該工具被稱爲代碼覆蓋,它是工具箱中的強大工具。 Jest 具備內置代碼覆蓋率,你能夠經過兩種方式激活:

  1. 經過命令行傳遞標誌「-coverage」
  2. 經過在 package.json 中配置 Jest

在使用 coverage 運行測試以前,請確保在 tests/filterByTerm.spec.js導入 filterByTerm

const filterByTerm = require("../src/filterByTerm");
// ...

保存文件並用 coverage 運行測試:

npm test -- --coverage

這是你獲得的結果:

PASS  __tests__/filterByTerm.spec.js
  Filter function
    ✓ it should filter by a search term (link) (3ms)
    ✓ it should filter by a search term (uRl) (1ms)
    ✓ it should throw when searchTerm is empty string (2ms)
-----------------|----------|----------|----------|----------|-------------------|
File             |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files        |     87.5 |       75 |      100 |      100 |                   |
 filterByTerm.js |     87.5 |       75 |      100 |      100 |                 3 |
-----------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total

這是對咱們的功能測試範圍的一個很好的總結。如你所見第3行被uncovered。你能夠嘗試經過測試我添加的新語句來達到100%的代碼覆蓋率。

若是你想保持代碼覆蓋率始終處於活動狀態,請在 package.json 中配置 Jest,以下所示:

"scripts": {
    "test": "jest"
  },
  "jest": {
    "collectCoverage": true
  },

你還能夠將標誌傳遞給測試腳本:

"scripts": {
    "test": "jest --coverage"
  },

還有一種能夠得到代碼覆蓋率的HTML報告的方法,它就像配置Jest同樣:

"scripts": {
    "test": "jest"
  },
  "jest": {
    "collectCoverage": true,
    "coverageReporters": ["html"]
  },

如今,每次運行 npm test 時,你均可以在項目文件夾中訪問名爲 coverage 的新文件夾:getting-started-with-jest/coverage/。在該文件夾中,你將找到一堆文件,其中 /coverage/index.html 是代碼覆蓋範圍的完整HTML摘要。

clipboard.png

若是單擊函數名稱,你還會看到確切的未經測試的代碼行:

clipboard.png

很整潔不是嗎?使用代碼覆蓋,你能夠在有疑問時發現要測試的內容

如何測試 React?

React 是一個很是流行的 JavaScript 庫,用於建立動態用戶界面。 Jest 能夠順利地測試 React 應用(Jest 和 React 均來自 Facebook 的工程師)。 Jest 也是 Create React App 中的默認測試器。

若是你想學習如何測試React組件,請查看測試React組件:最明確的指南。該指南涵蓋了單元測試組件、類組件、帶hook的功能組件和新的 Act API。

結論(從這裏開始)

測試是一個很大並且引人入勝的話題。有許多類型的測試和用於測試的庫。在這個 Jest 教程中,你學習瞭如何爲覆蓋率報告配置 Jest,如何組織和編寫簡單的單元測試,以及如何測試 JavaScript 代碼。

要了解有關 UI測試的更多信息,我強烈建議你查看用 Cypress 進行 JavaScript 端到端測試

即便它與 JavaScript 無關,我也建議閱讀 Harry Percival 的使用 Python 進行測試驅動開發。它包含了全部測試內容的提示和技巧,並深刻介紹了全部不一樣類型的測試。

若是你已準備好再邁出一步,要了解自動化測試和持續集成那麼JavaScript中的自動化測試和持續集成是爲你準備的。

你能夠在Github上找到本教程的代碼:getting-started-with-jest以及練習的解決方案。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索