javascirpt Jest 自動化測試

如何使用 Jest 做 javascript 自動化測試

林家慶 Jiaqing Lin 2020/11/19 16:53:44
2267

 

前言

Jest 是 Javascript 的測試框架,可用於 Babel, TypeScript, Node, React, Angular, Vue......等等開發的專案。

Jest 官方網站

 

安裝

使用 yarn 安裝 Jest

yarn add --dev jest

或是 使用 npm 安裝 Jest

npm install --save-dev jest

 

設定

在專案中的 package.json 的 script 屬性區塊,新增下面的部分

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

若無 package.json,請使用 npm init 指令來完成初始化設定

 

在完成設定之後,可以先在 terminal 上輸入

npm test

會得到這樣的結果

No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In /Users/jiaqing/Desktop/TPI
  3 files checked.
  testMatch: **/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x) - 0 matches
  testPathIgnorePatterns: /node_modules/ - 3 matches
  testRegex:  - 0 matches
Pattern:  - 0 matches
npm ERR! Test failed.  See above for more details.

發生錯誤,原因是找不到測試項目。

使用 Jest 做測試時,至少要有一個測試項目,否則就會報

 

現在,我們可以在專案中建立一個資料夾,放入要測試的檔案

在命名測試用的 JS 檔案時,檔名需要包含.test的字元,例如 math.test.js,因為Jest在執行時會偵測有.test字元的.js檔案,並對其做自動化測試

使用 Jest 做測試時,不需要特別import Jest元件,因為Jest可以在專案中進行全域的測試,

在新增完測試檔案後,我們可以開始使用 Jest 提供的方法進行測試

 

使用

Jest 提供了 test 方法,每個 test 方法代表一個測試案例,test 方法需要帶入兩個參數

第一個參數型別是string,也就是要輸入這個測試案例的名稱

第二個參數型別是function,在執行 test 期間會去呼叫此函式,並根據回傳結果是否拋錯來判斷測試結果

 

現在執行下面的測試案例

test("Hello world!", ()=>{

});

在 terminal 上輸入

npm test

會得到這樣的結果

(node:15321) ExperimentalWarning: The fs.promises API is experimental
 PASS  test/math.test.js
  ✓ Hello world! (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.578 s
Ran all test suites.

此測試案例結果為通過,可以從執行結果看到一些訊息,以下說明:

Test Suites

代表所有.test.js檔案,由於目前專案中只有一支.test.js,且該支.test.js的測試項目皆為通過,因此 1 passed, 1 total

Tests

代表所有測試案例,現在只有新增一筆測試案例,此測試案例為通過,因此 1 passed, 1 total

Snapshots

Jest提供UI測試時的工具,目前沒有使用,所以0 total

Time

執行完所有測試案例所花費的時間,目前花費時間為4.578s

 

此測試案例的函式雖然什麼也不做,仍能被判定通過

因為在執行測試時,會去呼叫我們所帶入的參數名稱和函式,但僅僅只是執行它而已

如果函式拋出錯誤,表示測試案例失敗

如果函式沒有拋出錯誤,表示測試案例成功

 

接下來繼續下個測試案例

我們保留上一個測試案例,然後新增一個新的測試案例

在執行的函示上,主動讓它拋出一個錯誤

test("test fail", ()=>{
    throw new Error("Fail");
});

test("Hello world!", ()=>{

});

結果

(node:15529) ExperimentalWarning: The fs.promises API is experimental
 FAIL  test/math.test.js
  ✕ test fail (2 ms)
  ✓ Hello world!

  ● test fail

    Fail

      1 | test("test fail", ()=>{
    > 2 |     throw new Error("Fail");
        |           ^
      3 | });
      4 | 
      5 | test("Hello world!", ()=>{

      at Object.test (test/math.test.js:2:11)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        3.413 s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

執行結果為失敗,我們可以從上面的訊息中,得知是在哪一行出錯,以及幾筆成功和幾筆失敗項目

 

接下來再提供一些測試案例

我們在專案的目錄新增一支 math.js 檔案

裡面寫了一個計算兩個變數相加的函示,再將它輸出

(P.S. 這邊使用 ES6 的寫法,若專案沒有使用 babel 之類的套件,也可以使用 ES5 的寫法)

const simpleCalculate = (a, b) =>{
    const total = a + b;
    return total;
}

export default simpleCalculate;

接下來在 .test.js 裡 import 這支函式,然後移除剛才兩個測試案例

接著在執行的第二個參數的函式中,呼叫剛才的相加函式,在裡面帶入5和10

並且在下面判斷,如果 total 不是15,就拋錯

然後執行

import simpleCalculate from '../math';

test("Caculate total", () => {
    const total = simpleCalculate(5, 10);
    if(total !== 15){
        throw new Error("Total Should be 15. But got "+ total);
    }
});

結果

(node:19783) ExperimentalWarning: The fs.promises API is experimental
 PASS  test/math.test.js
  ✓ Caculate total (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.747 s
Ran all test suites.

通過,因為5+10=15,所以不會報錯

 

接下來小小的改動一下相加的函示,在算式的後面偷偷加上1

const simpleCalculate = (a, b) =>{
    const total = a + b + 1;
    return total;
}

module.exports = { simpleCalculate }

然後執行測試

(node:19808) ExperimentalWarning: The fs.promises API is experimental
 FAIL  test/math.test.js
  ✕ Caculate total (2 ms)

  ● Caculate total

    Total Should be 15. But got 16

       6 |     const total = simpleCalculate(5, 10);
       7 |     if(total !== 15){
    >  8 |         throw new Error("Total Should be 15. But got "+ total);
         |               ^
       9 |     }
      10 | });
      11 | 

      at Object.<anonymous> (test/math.test.js:8:15)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.521 s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

毫不意外的失敗了,因為 total 現在等於16,因此進入了IF判斷,並且拋錯

 

由於目前的寫法是我們在參數的函式內寫了IF的判斷式,如果沒有得到想要的值,就直接拋錯,若符合預期就什麼也不做

缺點是,當測試案例的測試條件變多時,就必須在函示內用許多行數去處理這些判斷

關於這點,Jest有提供一個函式庫,可以用精簡的程式碼來執行我們所要的判斷

 

Jest 函式庫介紹

Jest的自動化測試可以簡單歸納成一句話:

「如果程式符合預期,就不會出錯,若否,則會引發錯誤。」

 

接下來示範 Jest 函式庫所提供的方法

expect(value)

在編寫測試時,通常需要檢查值是否滿足某些條件。 Expect可以呼叫許多 "matchers",並驗證不同的內容。

 

首先,先將剛才寫的 IF 判斷式註解,並把剛才的math.js裡的相加函式還原成原本的樣子

再使用 expect 方法,選擇 toBe 做為 matchers

import simpleCalculate from '../math';


test("Caculate total", () => {
    const total = simpleCalculate(5, 10);
    expect(total).toBe(15);
    // if(total !== 15){
    //     throw new Error("Total Should be 15. But got "+ total);
    // }
});

如程式碼所示,expect 參數帶入我們要檢查的值,並用 toBe 的參數去檢查是否等於我們所要的內容

如果翻成語句則是: 預期 total 是否等於15

如果是的話就不會出錯,若否,則會拋出錯誤

接著執行測試

(node:20217) ExperimentalWarning: The fs.promises API is experimental
 PASS  test/math.test.js
  ✓ Caculate total (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5.608 s
Ran all test suites.

通過,因為等於15所以沒有引發錯誤

 

接下來再將算式後面加1,讓其可以報錯

const simpleCalculate = (a, b) =>{
    const total = a + b + 1;
    return total;
}

module.exports = { simpleCalculate }

再執行一次

(node:20254) ExperimentalWarning: The fs.promises API is experimental
 FAIL  test/math.test.js
  ✕ Caculate total (5 ms)

  ● Caculate total

    expect(received).toBe(expected) // Object.is equality

    Expected: 15
    Received: 16

       5 | test("Caculate total", () => {
       6 |     const total = simpleCalculate(5, 10);
    >  7 |     expect(total).toBe(15);
         |                   ^
       8 |     // if(total !== 15){
       9 |     //     throw new Error("Total Should be 15. But got "+ total);
      10 |     // }

      at Object.<anonymous> (test/math.test.js:7:19)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        4.349 s
Ran all test suites.
npm ERR! Test failed.  See above for more details.

失敗,但是這次我們使用了 Jest 函示庫,因此有了更多的訊息

從上面顯示的名稱可以看到是哪個測試案例出錯了

接著下面顯示,原本預期得到15,但實際上得到了16,而這就是出錯原因

使用 expect 方法,我們可以得到更多的訊息,而且不必自己寫ErrorMsg

在函式庫中,還有提供許多方法,例如

.toBeNull()

檢查是否等於空值

.toBeLessThan()

檢查是否小於某個值

.toBeGreaterThan()

檢查是否大於某個值

......等

但比較常用的還是檢查是否相等某個值

 

異步程式碼測試

現在我們用 setTimeout 來寫異步程式,我們在 setTimeout 的 callback 裡,寫下一個檢查式

expect(1).toBe(2); //預期 1 等於 2

然後將 setTimeout 設為7秒

import simpleCalculate from '../math';

test("Async test", ()=>{
    setTimeout(()=>{
        expect(1).toBe(2);
    }, 7000)
})


test("Caculate total", () => {
    const total = simpleCalculate(5, 10);
    expect(total).toBe(15);
    // if(total !== 15){
    //     throw new Error("Total Should be 15. But got "+ total);
    // }
});

然後執行測試

(node:20532) ExperimentalWarning: The fs.promises API is experimental
 PASS  test/math.test.js
  ✓ Async test (1 ms)
  ✓ Caculate total (2 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.76 s
Ran all test suites.

令人意外的結果,通過

顯然出了一些問題,測試過程中沒有去等待7秒,就執行完了

因為檢查的判斷式在異步的 setTimeout 內,而測試過程僅僅只是執行 test 方法,到執行完成時沒有錯誤發生,因此測試通過

而解決這個問題的方法,需要額外增加一個參數,來告訴 Jest 這是一個同步的程式碼

test("Async test", (done)=>{
    setTimeout(()=>{
        expect(1).toBe(2);
        done();
    }, 7000)
})

在第二個參數函式的中,帶入一個參數 done ,並且在檢查式完成後去呼叫它

執行測試

(node:20563) ExperimentalWarning: The fs.promises API is experimental
 FAIL  test/math.test.js (7.638 s)
  ✕ Async test (5007 ms)
  ✓ Caculate total (2 ms)

  ● Async test

    : Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

      2 | import simpleCalculate from '../math';
      3 | 
    > 4 | test("Async test", (done)=>{
        | ^
      5 |     setTimeout(()=>{
      6 |         expect(1).toBe(2);
      7 |         done();

      at new Spec (node_modules/jest-jasmine2/build/jasmine/Spec.js:116:22)
      at Object.<anonymous> (test/math.test.js:4:1)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        10.121 s
Ran all test suites.

測試結果失敗,可以從錯誤資訊中得到這段測試總共花了10.121秒去完成,也就是說它確實有去等待我們在 setTimeout 中,所設定的7秒

然後完成了在 setTimeout callback 裡面的檢查式

還有其他比較進階的用法,可以做到這點

Promice

現在將原本的測試案例註解,並且在 math.js 裡的算式改成promice的形式

const simpleCalculate = (a, b) =>{
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(a < 0 || b < 0){
                return reject("參數需為正整數");
            }
            resolve(a + b)
        }, 2000);
    });
}

新增一個Promice的測試案例

import simpleCalculate from '../math';

test("Promice test", (done)=>{
    simpleCalculate(5, 10).then((sum) => {
        expect(sum).toBe(15);
        done();
    });
})

在Promice完成後,呼叫 done 來告訴 Jest 此異步測試執行完畢

執行測試

(node:20786) ExperimentalWarning: The fs.promises API is experimental
 PASS  test/math.test.js (5.105 s)
  ✓ Promice test (2005 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        8.289 s
Ran all test suites.

通過,結果符合預期

 

現在將預期結果改成16

import simpleCalculate from '../math';

test("Promice test", (done)=>{
    simpleCalculate(5, 10).then((sum) => {
        expect(sum).toBe(16);
        done();
    });
})

結果

(node:20790) ExperimentalWarning: The fs.promises API is experimental
 FAIL  test/math.test.js
  ✕ Promice test (2012 ms)

  ● Promice test

    expect(received).toBe(expected) // Object.is equality

    Expected: 16
    Received: 15

      4 | test("Promice test", (done)=>{
      5 |     simpleCalculate(5, 10).then((sum) => {
    > 6 |         expect(sum).toBe(16);
        |                     ^
      7 |         done();
      8 |     });
      9 | })

      at test/math.test.js:6:21

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        7.655 s
Ran all test suites.

失敗,可以得知 Jest 確實有透過新增的參數 done 去等待非同步程式碼完成

 

Async/Await

現在使用 async await 的非同步機制,來完成測試案例

先在第二個參數函式前,宣告其為async function

然後在函式內 await 加法函式,async await 機制會去等待這個函式執行完,才去執行下一行

取得 sum 之後,將其帶入 expect 並檢查是否等於15

由於 async await 機制可以讓程式逐步執行的關係,所以不需要帶入 done 參數來告訴 Jest 這個 test 是在什麼時候完成的

test("Async/Await test", async () => {
    const sum = await simpleCalculate(5, 10);
    expect(sum).toBe(15);
});

執行測試

(node:22359) ExperimentalWarning: The fs.promises API is experimental
 PASS  test/math.test.js (6.788 s)
  ✓ Async/Await test (2006 ms)
  ✓ Promice test (2002 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        8.911 s
Ran all test suites.

通過,使用 async await 依然可以讓非同步的測試正常運作

 

補充說明

在專案中的 package.json 的 script 屬性區塊,test屬性裡在 jest 後面增加 --watch

可以讓我們在每次跑完測試時,不會自動回到 terminal ,可以持續監控變更

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

執行測試

(node:20426) ExperimentalWarning: The fs.promises API is experimental
 PASS  test/math.test.js
  ✓ Caculate total (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        5.168 s
Ran all test suites.

Watch Usage
 › Press f to run only failed tests.
 › Press o to only run tests related to changed files.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.

現在下方列出了一些監控模式的選項,例如按下 a 可以重新再跑一次測試,按下 q 可以離開監控模式,按下 w 可以重新打開監控選項

這些指令方便我們不用再重新回到 termimal 操作,使用者可自行按照情境需求來決定是否使用這個功能

 

 

結尾

自動化測試的介紹到此結束,Jest 是個非常強大的工具

上面提到的方法僅僅是 Jest 的其中一小部分,在函式庫內還有許多好用的API

Jest 也提供前端 UI 上的測試,可自行到官網查閱

希望這篇文章對於想學習前端 JS 自動化測試的人有所幫助

在編寫文章時,也幫助自己釐清關於環境設定,使用方式等等,一些以前還有點模糊的部分

如文章內容講述有誤,請不吝在下面留言指教,謝謝

 

 

參考資料

Jest 官網文件

Udemy - The Complete Node.js Developer Course (3rd Edition)

 

林家慶 Jiaqing Lin