iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 17
1
Modern Web

玩轉 Storybook系列 第 17

玩轉 Storybook: Day 17 Unit Test with JEST

用 Storybook 搭配測試方法,從元件開發到完成整個專案,可以增加開發者的信心,不會擔心改A錯B的狀況發生,因為從邏輯面及UI面都能被完整的測試到。

單元測試可以驗證元件的邏輯功能,驗證元件的Outup結果在給定固定的Input情況下仍保持不變。

Vue Test Utils

Vue Test Utils 是 Vue 官方提供的測試套件,它是以 Jest 為基礎。它提供二個方法讓我們在測試程式中渲染元件,一個是mount,一個是shallowMount

差異處在於,Vue Component 常常會有子父層關係,使用mount 會真實將所有子層渲染出來;shallowMount則只渲染該元件,不渲染子層們,可以避免重複渲染讓我們的測試變慢。

當做 Unit Test 時,建議使用 shallowMount,在做整個專案的 Test 時,則是使用 mount。

Writing Test

shallowMount第一個參數是測試的 component ,第二個參數可以自行選擇是否使用,可依照 Vue Test Utils 提供的 options 放入一個 object 。在這邊我們使用propsData做為提供給元件的props。

// tests/unit/button.spec.js

import { shallowMount } from "@vue/test-utils";
import Button from '@/stories/Button.vue';

describe("Button.vue", () => {

  it("Renders button text using a label prop", () => {
    const label = "Test Button";
    const wrapper = shallowMount(Button, {
      propsData: { label }
    });
    expect(wrapper.text()).toBe('Test Button');
  });

});
$ npm run test:unit tests/unit/button.spec.js

如果我們想要再測試 Button 如果指定 primary 為 True,元件渲染成有 Primary Class

  it("Renders the button in the primary state", () => {
    const label = "Button";
    const primary = true;
    const wrapper = shallowMount(Button, {
      propsData: { label, primary }
    });
    expect(wrapper.classes('storybook-button--primary')).toBe(true)
  });

shallowMount 的優化

繼續測試 Button 如果指定 primary 為 False,元件渲染為 Secondary Class

上述兩個 test 皆使用到 propsData ,因此我們可以創立一個回傳物件的函式:factory,然後簡化我們的 Test Case。

  const factory = (propsData, label = 'Button') => {
    return shallowMount(Button, {
      propsData: {
        label,
        ...propsData
      }
    })
  };
  
  ...
  
  it("Renders the button in the primary state", () => {
    const wrapper = factory({primary: true});
    expect(wrapper.classes('storybook-button--primary')).toBe(true)
  });

  it("Renders the button in the secondary state", () => {
    const wrapper = factory({primary: false});
    expect(wrapper.classes('storybook-button--secondary')).toBe(true)
  });

Storybook addon Jest

加上這個 Addon 後我們可以在 Storybook 上看到測試結果,並搭配元件當下呈現的畫面。

Setup

$ npm install --save-dev @storybook/addon-jest
  • Jest Configuration

當我們在跑測試的時候,結果除了呈現在 Terminal,也可以輸出成JSON的檔案。

在 package.json 加上 test:generate-output script

// package.json

"scripts": {
  "test:unit": "vue-cli-service test:unit",
  "test:generate-output": "vue-cli-service test:unit --json --outputFile=.jest-test-results.json",
}

然後記得把.jest-test-results.json 加入 .gitignore

Register

  • 把 jest 的 addon 加入 Storybook 中
// .storybook/main.js

module.exports = {
  addons: ['@storybook/addon-jest'],
};
  • .jest-test-results.json 的結果 使用 Decorator 加入到整個 Storybook 的設定
// .storybook/preview.js

import { addDecorator } from '@storybook/vue';
import { withTests } from '@storybook/addon-jest';
import results from '../.jest-test-results.json';

addDecorator(
  withTests({
    results,
  })
);
  • 在 Story 中 加上 Parameter,指定要呈現的測試檔案
// stories/Button.stories.js

export default {
  title: 'Example/Button',
  component: MyButton,
  ...
  parameters: {
    jest: ['button.spec.js'],
  },
};

顯示測試結果在 Addon 上面

繼續開發

為了保證修改元件,也能持續不斷的符合測試結果,我們可以在開發時期,把單元測試的指令使用 watch 模式,讓測試結果持續輸出。

$ npm run test:generate-output -- --watch

接下來修改 Button.vue,故意調整到 Primary 的結果

computed: {
  classes() {
    return {
      'storybook-button': true,
      // 原本是 this.primary 在前面加上 !
      'storybook-button--primary': !this.primary, 
      ...
    };
  },
}    

測試結果,會抓到Primary的State沒有成功Render出來

在 Storybook 頁面也會看到 Primary Button 的顯示有問題,單元測試的結果也列在 Addon Panel 的 Test 分頁之下

當每次修改我們發現測試失敗,因為是單元測試,可以在最小的修改範圍追查失敗的結果。

修改完成後,我們可以重新跑測試,可以選擇 a 跑全部的測試,也可以選擇 f 只跑失敗的測試。

現在修改成功,Storybook上也都標示Passed通過測試

試看看

TaskList.spec.js

//tests/unit/TaskList.spec.js
import Vue from 'vue';
import TaskList from '@/components/TaskList.vue';
import { withPinnedTasksData } from '@/components/TaskList.stories';
import { mount } from "@vue/test-utils";

describe("TaskList.vue", () => {

  it('Renders pinned tasks at the start of the list', () => {
    const wrapper = mount(TaskList, {
      propsData: { tasks: withPinnedTasksData }
    });
    const firstTaskPinned = wrapper.find('.list-item:nth-child(1).TASK_PINNED');
    expect(firstTaskPinned.exists()).toBe(true)
  });

});

TaskList.stories.js

//src/components/TaskList.stories.js

export default {
  title: "TaskList",
  ...
  parameters: {
    jest: ['TaskList.spec.js'],
  },
};

Run Test

結語

Unit Test 完整程式碼:https://git.io/JUwJD

單元測試是為了保證,按照預期輸入時,會有預期輸出,非常適合測試元件的功能邏輯,但是單元測試需要開發者自行撰寫測試案例,不會在所有的元件上都實作單元測試,只會對是業務重點邏輯或是重用性很高的元件程式實作。

Next

下一章要介紹的視覺回溯測試,會使用 Chromatic 自動化工具做視覺回溯測試,這種測試方式不需要寫測試腳本,而是自動會去檢查及比較前後的變更在視覺上是否發生變化,也會列出所有被影響到的元件,開發時期做變更的信心也會更加的穩固,也更容易擁抱變化。

Reference

Storybook addon Jest

Vue Test Utils

從範例學習 Vue.js 的 Unit Test

JEST 單元測試學習筆記


上一篇
玩轉 Storybook: Day 16 Addons - Viewport/Accessibility
下一篇
玩轉 Storybook: Day 18 Automated Visual Testing
系列文
玩轉 Storybook30

尚未有邦友留言

立即登入留言