iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 18
2
Modern Web

在 React 生態圈內打滾的一年 feat. TypeScript系列 第 18

Day17 | 不知道對不對,就把邏輯通通測起來 feat. Jest

  • 分享至 

  • xImage
  •  

前言

單元測試是個很神奇的技能,筆者一開始是為了重構而學的,那時候單純的以為寫下測試只是方便讓邏輯不被改變,但是沒想到它帶來的好處比想像中的還多,因為在為程式寫測試時,往往能看到 coding 看不到的另一面。

本篇會學習 Jest 的基本用法,後面幾章會再說明如何將測試環境帶入 React 中。


前置準備

  1. 文中的專案會以 Day16 的專案架構繼續講解,如果未跟到前一天的進度,可以從 GitHub 上 Clone 下來。
  2. 一顆擁有學習熱忱的心。

單元測試

單元測試是指為專案中每個單一行為做測試,只要每個單一行為都沒問題,那就能確保邏輯是正常的,

且留下的測試案例也可以在團隊討論或交接時更清楚每一個都行為都是為了什麼

就算是幾個月後的自己在 Review Code,也不用再猜測說「它大概會這樣回傳?」或是「可能是這樣子用?」。

好的測試案例甚至是一份最優秀的「工程師版本操作手冊」。

Jest

前端的測試框架不是只有 Jest,選擇原因是因為 Jest 和 React 的整合度較佳。除了 Jest 以外,常聽到的測試框架還有 mocha 。

使用方法

安裝 Jest

輸入以下指令安裝 Jest :

npm install jest --save-dev

建立測試案例

首先在專案的根目錄,也就是與 src 同一層的地方,建立一個新目錄__tests__:

|-src
|-__tests__

今後我們所有的測試案例都會在該目錄中撰寫,而 Jest 在執行測試時,會自動尋找檔名中含有 .test 的文件進行測試,所以我們在__tests__ 目錄下建立一個 index.test.js 作為一份將要被測試的檔案。

|-src
|-__tests__
 |-index.test.js

將__tests__/index.test.js 打開,並寫下一個簡單的測試案例:

test('Check the result of 5 + ', () => {
  expect(5 + 2).toBe(7);
});

把上方的 test 當作一個 function,負責描寫一個測試案例,它擁有兩個參數:

  1. 第一個參數為「測試名稱」,能夠簡單描述這部分是在測試什麼邏輯或功能。
  2. 第二個參數是一個 function,又稱斷言,function 內的 expect 用來描述被測試的內容, toBe 是測試內容的回傳值是否符合期望值,例如上方的測試內容為「 5 加上 2 期望會等於 7 」。

然後當你寫下的時候會發現,怎麼 ESLint 會紅成這樣子:

那是因為 testexpect 都不是 JavaScript 原生提供的 Method ,所以被 ESlint 看見就會報錯,這裡先打開 ESlint 的設定檔 .eslintrc.js ,增加 Jest 執行環境:

module.exports = {
  env: {
    browser: true,
    es6: true,
    jest: true,
  },
  /* 其餘省略 */
}

再回到__tests__/index.test.js 就能看見警告消失了:

現在我們已經完成第一個測試案例了,我們可以用指令 jest 執行它,但考慮到我們是將 Jest 不是裝在全域環境中,以及今後測試方便,還是先打開 package.json,將執行測試的指令加到 script

"scripts": {
  /* 其餘省略 */
  "test": "jest",
},

加完後便可輸入 npm run test 來執行第一次測試:

結果內會顯示 Jest 測試了哪些 .test.js 檔案,還有每個測試( expect )內的結果( toBe )是否正確符合,符合的話會輸出 PASS。

我們再打開__tests__/index.test.js,將 toBe(7) 改成 8,並重新執行測試:

結果會狠狠地出現紅色的錯誤,並且告訴你程式運行後的結果應該是 7,不是 8。

除了 toBe 這個斷言外,還有許多其他的斷言方式,例如物件的話可以用 toEqual,想得到相反的結果則是在斷言前串上一個 not

test('Check the result of 5 + 2', () => {
  expect(5 + 2).not.toBe(8);
});

測試結果就會變成正確,因為加上 not 後就變成「不等於 8」。

到這裡也許會覺得很奇怪,為什麼要為一個早就知道結果是正確的東西做測試,沒意義不是嗎?

其實不能這樣想,因為就現在而言,Function 就是長那個樣子,輸入什麼就輸出什麼,所以才會覺得為了不會變的結果寫下測試按理很多餘。

但是三天後,五天後,或是十天後呢?它都還是一樣不變嗎?

程式其實是長遠的,寫下多多的程式就會埋下多多的 Bug,就算沒有 Bug,客戶的要求也是一堆,相信各位都一定會有改了這個,壞了那個的經驗吧?

寫下測試就像買了保險,儘早替專案埋入測試環境,才能夠保證在每次更改的時候,Function 的運行結果都不會出乎你意料之外。

測試的作用域

如果我們要寫下完整的測試案例,也許就需要將每一項測試分類,並且引入作用域,而 describe 可以做到這件事情,它的結構和 test 相同,擁有兩個參數,一個是描述該作用域的測試內容,第二個一樣是個 Function,我們會將同作用域的測試都放入其中,例如:

describe('Check add', () => {
  test('Check the result of 5 + 2', () => {
    expect(5 + 2).not.toBe(8);
  });

  test('Check the result of 5 + 3', () => {
    expect(5 + 3).toBe(8);
  });
});

describe('Check sub', () => {
  test('Check the result of 5 - 2', () => {
    expect(5 - 2).not.toBe(1);
  });

  test('Check the result of 5 - 3', () => {
    expect(5 - 3).toBe(2);
  });
});

上方一口氣新增了另外三個測試案例,雖然都沒有意義,但是根據加法和減法替它們用 describe 做分類,執行測試的結果也會稍微不同:

當然作用域不只是這樣子美觀分類而已,在作用域內還有幾個生命週期可以調用:

  1. beforeAll :所在區域內會第一個執行。
  2. beforeEach :每一次的測試前會先執行。
  3. afterAll :所在區域內最後一個執行。
  4. afterEach :每一次的測試後會馬上執行。

例如我們在 Check add 的 describe 中使用 beforeEachafterAll ,並讓它們在 console 中留下文字:

describe('Check add', () => {
  beforeEach(() => {
    console.log('每次執行測試前執行哦');
  });

  afterAll(() => {
    console.log('所有測試結束後才看得見我');
  });

  test('Check the result of 5 + 2', () => {
    expect(5 + 2).not.toBe(8);
  });

  test('Check the result of 5 + 3', () => {
    expect(5 + 3).toBe(8);
  });
});

/* 其餘省略 */

執行測試後可以看到生命週期:

通常我們會使用 afterEach,在每一次測試後重置一些因為測試出現的副作用,像是重置初始資料等等,之後的篇章會再使用到它。

測試 Function

到目前為止,本文的案例為了方便講解,都直接以一個結果,像是「5 + 2」,然後去斷言是不是 7,但是現實中是不會有這種情況的,就像文章中一開始提到的,單元測試是以一個最小單一行為去做測試,有可能是個 function,也有可能是整個 module,因此本文最後展示如何直接使用 function 做測試。

在 src 下創建 utils,我們會將所有的共用方法都放在該目錄下:

|-src
 |-utils
 /* 其餘省略 */

然後在 utils 下新增一個 math.js,並創建兩個 function,分別是加法和減法,記得要將它們 export:

export const add = (a, b) => a + b;

export const sub = (a, b) => a - b;

回到 index.test.js ,將 src/utils/math.js 的 addsub import:

import { add, sub } from '../src/utils/math';

然後將 Check sub 和 Check add 中的加減法都替換成 addsub 執行,以下只展示修改後的 Check sub , Check add 讓大家自行練習,文章中的最後也有提供 GitHub 可以看範例程式碼:

describe('Check sub', () => {
  test('Check the result of 5 - 2', () => {
    expect(sub(5, 2)).not.toBe(1);
  });

  test('Check the result of 5 - 3', () => {
    expect(sub(5, 3)).toBe(2);
  });
});

改完後輸入指令執行會發生錯誤:

因為測試不會讓 index.test.js 經過編譯,所以讀到 import 這個語法就出錯了,這時候我們可以手動在專案目錄下新增一個 .babelrc.js,並在裡面輸入之前下載來負責編譯 ES6 語法的 preset:

module.exports = {
  presets: ['@babel/preset-env'],
};

這麼一來,將測試案例會在執行時自動經過編譯,結果也就不會有問題了。

本文的範例程式碼會提供在 GitHub 上,歡迎各位參考:)


結尾

關於測試的基本本來一篇文章就想講完,但是不知不覺就越打越長,到最後連 Mock 的觀念都沒有提到,因此決定將 Mock 抽離到下一篇文章,也還好當時有預留一個位置,剛好在這時候補上。

如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!


上一篇
Day16 | SPA 的換頁不是你的換頁
下一篇
Day18 | 用 Mock 打造國家機器,驗證函式執行 feat. jest
系列文
在 React 生態圈內打滾的一年 feat. TypeScript31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
連城
iT邦新手 4 級 ‧ 2020-06-09 18:04:32

我覺得下次可以考慮跟TDD做搭配
TDD 會顛覆你的想法

神Q超人 iT邦研究生 5 級 ‧ 2020-06-18 14:06:40 檢舉

太感人了,你居然一篇一篇文章看 /images/emoticon/emoticon02.gif
告訴我你在哪裡,說不定可以當個朋友 XD

連城 iT邦新手 4 級 ‧ 2020-06-19 13:34:08 檢舉

我是在台北阿XD
https://github.com/nttu94507/React2
這是我參照你文章寫code的github
與你的不同是
系統與套件版本的不同
我把以前做過的東西盡量都留下
順便練習git 的基礎用法

BTW 對於新手來說
這樣step by step 的方式很好
雖然有點小缺點但可以忽略不計了

神Q超人 iT邦研究生 5 級 ‧ 2020-06-19 17:38:41 檢舉

你真的太讚了,直接實作起來!
我真的希望這個系列對大家有幫助 /images/emoticon/emoticon02.gif

哈哈哈哈 小缺點的部分再麻煩你告訴我了 /images/emoticon/emoticon37.gif

BTW 我卻是在台南 XD

0
kika
iT邦新手 3 級 ‧ 2021-07-04 18:02:48

Hi 神Q大大
我嘗試在vscode建立 .babelrc.js,但是不被允許
所以上網查了一下jest官網,現在可以用設定babel.config.js解決~
資料來源: https://jestjs.io/docs/tutorial-react

連城 iT邦新手 4 級 ‧ 2021-08-18 15:02:03 檢舉

我兩種都可以
babel.config.js
.babelrc.js

我要留言

立即登入留言