【譯】Jest 初學者教程:JavaScript 測試入門

前端時空發表於2020-04-04

來源 | AlloyTeam

作者 | zhongzhong

地址 | http://www.alloyteam.com/2020/02/14255

轉載 | 前端時空

測試的意義是什麼?

在程式設計術語中,測試意味著檢查我們的程式碼是否符合某些期望。例如:一個名為transformer的函式應在給定某些輸入的情況下返回期望的輸出。

測試型別很多,但簡單來說測試分為三大類:

  • 單元測試
  • 整合測試
  • UI 測試

在本 Jest 教程中,我們將僅介紹單元測試,但是在本文結尾,您將找到其他型別的測試的資源。

Jest 教程:什麼是 Jest

JestJavaScript 測試執行程式,即用於建立,執行和構建測試的 JavaScript 庫。Jest是作為 NPM 軟體包分發的,您可以將其安裝在任何 JavaScript 專案中。 Jest 是目前最受歡迎的測試執行程式之一(我覺得沒有之一),也是 Create React App 的預設選擇。

首先,我怎麼知道要測試什麼?

在測試方面,即使是簡單的程式碼塊也可能使新手懵逼。最常見的問題是 “我怎麼知道要測試什麼?”。如果你正在編寫 Web 應用程式,那麼一個好的切入點就是測試應用程式的每個頁面以及每個使用者的互動。但是,Web 應用程式也由功能和模組之類的程式碼單元組成,也需要進行測試。大多數情況下有兩種情況:

  • 你繼承了未經測試的舊程式碼
  • 你從 0 開始新實現的功能

該怎麼做呢?對於這兩種情況,你都可以通過將測試視為程式碼的一部分來進行檢查,這些程式碼可以檢查給定的函式是否產生預期的結果。典型的測試流程如下所示:

  • 匯入到測試的功能
  • 給定一個輸入
  • 定義期望的輸出
  • 檢查函式是否產生預期的輸出

真的,就是這樣。如果你從以下角度考慮,測試將不再可怕:輸入-預期輸出-宣告結果。稍後,我們還將看到一個方便的工具,用於幾乎準確地檢查要測試的內容。現在先用 Jest 手動測試!

Jest 教程: 初始化專案

與每個 JavaScript 專案一樣,您將需要一個 NPM 環境(確保在系統上安裝了 Node)。建立一個新資料夾並使用以下命令初始化專案:

mkdir getting-started-with-jest && cd 

npm init -y 
複製程式碼

下一步安裝 Jest:

npm i jest --save-dev
複製程式碼

我們還需要配置一個 script,以便從命令列執行測試。開啟 package.json 並配置名為 test 的指令碼以執行 Jest

"scripts": {
    "test""jest"
  },
複製程式碼

現在,你可以嗨皮的開始(入坑)了。

Jest 教程:規範和測試驅動的開發

作為開發人員,我們都喜歡創造自由。但是,當涉及到嚴重的問題時,大多數時候沒有那麼多特權。通常,我們必須遵循規範,即對構建內容的書面或口頭描述。

在本教程中,我們的專案經理提供了一個相當簡單的規範。非常重要的一點是,業務方需要一個 JavaScript 函式,該函式用來過濾一個物件陣列。

對於每個物件,我們必須檢查一個名為 url 的屬性,如果該屬性的值與給定的關鍵字匹配,則應在結果陣列中包括匹配的物件。作為一個精通測試的 JavaScript 開發人員,你希望遵循 TDD(測試驅動開發),這是一種在開始編寫程式碼之前必須編寫失敗測試的準則。

預設情況下,Jest 希望在專案資料夾中的 tests 資料夾中找到測試檔案。 建立新資料夾:

cd getting-started-with-jest
mkdir __tests__
複製程式碼

接下來,在 __tests__ 目錄中中建立一個名為 filterByTerm.spec.js 的新檔案。你可能想知道為什麼副檔名包含 .spec這是從 Ruby 借來的約定,用於將檔案標記為給定功能的規範。

現在,讓我們進行測試!

Jest 教程:測試結構和第一個失敗的測試

是時候建立你的第一個 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
  });
});
複製程式碼

至此,我們準備編寫測試了。請記住,測試是輸入,功能和預期輸出的問題。首先讓我們定義一個簡單的輸入,即物件陣列:

  test("it should filter by a search term (link)", () => {
    const input = [
      { id1url"https://www.url1.dev" },
      { id2url"https://www.url2.dev" },
      { id3url"https://www.link3.dev" }
    ];
  });
});
複製程式碼

接下來,我們將定義預期的結果。根據規範,被測函式應忽略其 url 屬性與給定搜尋詞不匹配的物件。例如,我們可以期望一個帶有單個物件的陣列,給定 link 作為搜尋詞:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id1url"https://www.url1.dev" },
      { id2url"https://www.url2.dev" },
      { id3url"https://www.link3.dev" }
    ];
 
    const output = [{ id3url"https://www.link3.dev" }];
  });
});
複製程式碼

現在我們準備編寫實際的測試。我們將使用 expectJest 匹配器來檢查虛擬函式(目前)在呼叫時是否返回了預期結果。這是測試:

expect(filterByTerm(input, "link")).toEqual(output);
複製程式碼

為了進一步分解內容,這是在程式碼中呼叫該函式的方式:

1filterByTerm(inputArr, "link");
複製程式碼

Jest 測試中,你應該將函式呼叫包裝在 expect 中,並與匹配器(用於檢查輸出的 Jest 函式)一起進行實際測試。這是完整的測試:

describe("Filter function", () => {
  test("it should filter by a search term (link)", () => {
    const input = [
      { id1url"https://www.url1.dev" },
      { id2url"https://www.url2.dev" },
      { id3url"https://www.link3.dev" }
    ];
 
    const output = [{ id3url"https://www.link3.dev" }];
 
    expect(filterByTerm(input, "link")).toEqual(output);
 
  });
});
複製程式碼

(要了解有關 Jest 匹配器的更多資訊,請查閱文件)。

到這裡,你可以嘗試:

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.`` 實際上,這是好事,讓我們在下一節中修復它。

Jest 教程: 修復測試(並再次讓它失敗)

真正缺少的是 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 = [
      { id1url"https://www.url1.dev" },
      { id2url"https://www.url2.dev" },
      { id3url"https://www.link3.dev" }
    ];
 
    const output = [{ id3url"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 = [
      { id1url"https://www.url1.dev" },
      { id2url"https://www.url2.dev" },
      { id3url"https://www.link3.dev" }
    ];
 
    const output = [{ id3url"https://www.link3.dev" }];
 
    expect(filterByTerm(input, "link")).toEqual(output);
 
    expect(filterByTerm(input, "LINK")).toEqual(output); // New test
 
  });
});
複製程式碼

執行測試,你會發現測試失敗了,讓我們再次修復它。

Jest 教程: 修復大小寫問題

filterByTerm 還應考慮大寫搜尋詞。換句話說,即使搜尋詞是大寫字串,它也應返回匹配的物件:

filterByTerm(inputArr, "link");
filterByTerm(inputArr, "LINK");
複製程式碼

為了測試這種情況,我們引入了一個新的測試:

expect(filterByTerm(input, "LINK")).toEqual(output); // New test
複製程式碼

為了使其通過,我們可以調整提供的正規表示式以匹配:

//
    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 = [
      { id1url"https://www.url1.dev" },
      { id2url"https://www.url2.dev" },
      { id3url"https://www.link3.dev" }
    ];
 
    const output = [{ id3url"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);
  });
}
複製程式碼

再次執行並看到它通過。做得好!作為練習,你可以編寫兩個新測試並檢查以下條件:

  • 測試搜尋字詞 uRl
  • 測試一個空的搜尋詞。函式應如何處理?

你將如何組織這些新測試?

在下一節中,我們將看到測試中的另一個重要主題:程式碼覆蓋率。

Jest 教程: 程式碼覆蓋率

什麼是程式碼覆蓋率?在談論它之前,讓我們快速調整我們的程式碼。在專案根目錄 src 中建立一個新資料夾,並建立一個名為 filterByTerm.js 的檔案,我們將在其中放置和匯出函式:

mkdir src && cd _$
touch filterByTerm.js
複製程式碼

filterByTerm.js 內容:

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 具有內建的程式碼覆蓋範圍,你可以通過兩種方式啟用它:

  • 通過命令列傳遞標誌 --coverage
  • package.json 中配置 Jest

在進行覆蓋測試之前,請確保將 filterByTerm 匯入 tests / filterByTerm.spec.js

const filterByTerm = require("../src/filterByTerm");
// ...
複製程式碼

儲存檔案並進行覆蓋測試:

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
複製程式碼

對於我們的函式,一個非常好的總結。可以看到,第三行沒有被測試覆蓋到。下面來通過新增新的測試程式碼,讓覆蓋率達到 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 的新資料夾:jest / jest / coverage /。在該資料夾中,您會發現一堆檔案,其中/coverage/index.html 是程式碼覆蓋率的完整 HTML 摘要:

如果單擊函式名稱,你還將看到確切的未經測試的程式碼行: 非常整潔不是嗎?通過程式碼覆蓋,你可以發現有疑問時要測試的內容。

精彩文章

理想主義團隊的開源作品之Chameleon跨端框架
React 中必會的 10 個概念
一道面試題引發關於 js 隱式轉換的思考
前端首屏耗時測量方法
一分鐘理解 JavaScript 釋出訂閱模式
前端響應式你瞭解多少?


喜歡我們的小夥伴
請點選點贊、分享、評論喲
關注我們的公眾號,學習知識不迷路

【譯】Jest 初學者教程:JavaScript 測試入門

回覆「0」進入交流群

回覆「1」看每日一題

回覆「2」看答案解析

相關文章