第十二集: 從零開始實現一套pc端vue的ui組件庫( jest單元測試 )

第十二集: 從零開始實現( jest單元測試 )

1.聊聊測試

    本次我會與你們分享一下我學測試時候記的筆記知識以及本次項目裏面作的幾個測試.
    前端代碼的單元測試與集成測試屬於雷聲大雨點小, 不少人一提到它都說是個好東西, 試問又有幾個公司的vue項目是嚴格要求跑單元測試與集成測試的那?? 測試沒經過是否暫停上線? 除了大公司沒有幾家作獲得吧, 畢竟大多數公司只是讓專業的測試團隊進行'人肉測試'.
    如今前端體系搞得好龐大, 圍繞着前端開發的技術與知識點層出不窮, 更別說各類技術之間那剪不斷理還亂的糾葛, 我聽有人說過: "我只想好好寫前端代碼, 其餘的無論行不行", 這句話是個病句, 這些雜七雜八的技術也都是前端技術, 若是你只會寫你所謂的'前端代碼', 那你真的只能是一生'初學者'了┑( ̄Д  ̄)┍.
    對於這我的人都說好, 可是人人不咋用是咋回事那??🙅‍♂️接下來咱們就他的優缺點進行羅列.前端

2.優缺點

缺點vue

  1. 前端的測試技術體系還未成形, 本套ui用的就是vue-cli集成的jest 真心很差用....
  2. 有必定的學習成本, 我面試過不少6年以上經驗的, 連'設計模式'都搞不懂, 更別說讓他學測試了...
  3. 無關緊要的處境, 不少工程沒有測試跑的好好的, 寫了反而bug多多
  4. 不想進步的人的阻攔, 真別小看這條, 不少技術人員會製造各類理由, 不想跳出溫馨區.
  5. 每次改需求或是優化代碼, 則都須要改兩份代碼, 人力消耗大.

優勢node

  1. 多一種思考維度, 多一門技術護身, 對於要以技術養家的人來講, 這條也很重要.
  2. 爲主體邏輯的暢通保駕護航, 整套測試能跑下來就不會有太大的錯誤
  3. b格高, 讓別人看了能放心用你的東西, 這也是硬實力

3.用法與分類

大致上分爲兩類:git

  1. BDD 把全部邏輯都寫好, 而後根據你的總體邏輯制定你的測試, 好處固然是好理解,更有總體思惟, 缺點就是覆蓋率低, 並非很保險.
  2. TDD 把測試寫好再進行開發, 這個模式挺有意思, 先寫測試, 也就是在腦中先總體佈局, 每一步都是本身思考好了再去作的測試覆蓋率多是100%, 他的缺點就太明顯了, 開發人員技術必須硬, 並且若是改需求...有的忙了.

基本搭建
我是在vue項目裏面直接選擇的jest測試
單獨實驗的朋友能夠自行安裝 npm i jest -D
去配置一下github

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

命令行裏運行npm run test便可
若是電腦運行說沒有這個命令的話, 能夠用npx 或者在全局安裝一下
若是是es項目的話, 要集成一下 npm i @babel/core @babel/preset-env -D
jest內置了 對babel的依賴, 他看到.babelrc就會去配合解析的面試

基本使用
jest 會自動查找 xx.test.js的文件, 配置以下
隨便修改爲你喜歡的語義化就好, element-ui採用的是spec
這面這個文件能夠經過, jest init生成
jest.config.jsvue-cli

testMatch: [
    '**/tests/unit/**/*.(spec|test).(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
  ],
// 1: 最外層describe至關於一個大的父容器盒子, 把測試進行分'塊'
// 在出錯的時候, 控制檯會報出是哪一'塊'出錯了
describe('按鈕相關代碼', () => {
// 2: '小塊'測試單元, 具體的某些職責的測試, 
  test('測試 按鈕點擊小夥', () => {
// 3: 斷言, 也就是真正判斷某些值是否正確的一步
    expect(1).toBe(1);
  });
});

以偶上述爲例npm

// 意思就是判斷, 1 是否 === 1
expect(1).toBe(1);
// 由此可知, expect函數負責接收要測試的值
// toBe則爲 所謂的 ===, 與他裏面的值進行比較
// 那既然有 === 確定就會有更多種類型的判斷了
// 他學名叫配置器

多種類型的'配置器'element-ui

  1. toEqual 並非 == 嚴格說他是忽略引用,只比內容,內部估計是作了序列化 因此 {a:1}.toEqual({a:1}) true
  2. toBeFalsy 能否轉化爲 false
  3. toBeTruthy 能否轉化爲true
  4. toBeUndefined 是 undefined
  5. toBeDefined 不是 undefined
  6. toBeNull === null
  7. not 翻轉修飾符, expect(1).not.toBe(2); 1不是2
  8. toBeGreaterThanOrEqual(3) 大於等於3
  9. toBeLessThanOrEqual(3) 小於等於3
  10. toBeLessThan(3) 小於3
  11. toBeGreaterThan(3) 大於3
  12. 'abc'.toMatch('b') // 是否包含'b'字符串, 能夠寫正則

生命週期設計模式

beforeEach(() => {
  // 每一個test執行以前都會執行我
});

afterEach(() => {
// 每一個test執行以後都會執行我
});

beforeAll(() => {
// 全部test執行以前執行我
});

afterAll(() => {
//   全部test都執行完執行我
});
describe('按鈕相關代碼', () => {
  test('測試 按鈕點擊小夥', () => {
    expect(1).toBe(1);
  });
});

這個時代全部插件的配置都趨於'函數化'
上面的生命週期函數很符合設計模式, 咱們在寫項目的時候也能夠借鑑一下.

看完上面這些是否是感受測試頁很容易, 坑的在後面結合vue項目時.

4.vue裏面

vue裏面固然天差地別, 渲染方式都不同了, 這個還好有vue本身團隊提供的支持

介紹幾個vue裏面的概念

  1. mount: 能夠理解我vue裏面的實例化組件的方法, 官網這麼說:'建立一個包含被掛載和渲染的 Vue 組件的 Wrapper', 也就是一個完整的渲染, 他的優勢就是完整, 可是缺點也明顯就是效率低
  2. shallowMount 和 mount 同樣,建立一個包含被掛載和渲染的 Vue 組件的 Wrapper,不一樣的是被存根的子組件。也就是僅僅掛載當前組件實例;
  3. Wrapper 是一個包括了一個掛載組件或 vnode,以及測試該組件或 vnode 的方法。直觀點講就是專門用於測試的實例

因爲篇幅有限, 我就直接拿我工程裏面的舉例子了;
其實到底要測些什麼這方面, 我理解的也不是很透, 因此只是簡單的幾個例子, 一塊兒學習一塊兒討論.
按鈕組件
按鈕的測試
vue-cc-ui/tests/unit/Button.test.js

// shallowMount是@vue/test-utils官方提供的測試工具
import { shallowMount } from '@vue/test-utils';
import Button from '../../src/components/Button';
// 這是參考網上封裝的獲取dom的方法, 下面會有說明👇
import { findTestWrapper } from '../utils/util';


describe('測試button組件', () => {

  it('1: 能夠渲染出button組件', () => {
// 利用shallowMount實例化個人button組件
    const wrapper = shallowMount(Button);
// 關鍵詞contains, 判斷 Wrapper 是否包含了一個匹配選擇器的元素或組件。
// 也就是我想判斷, 這個button組件渲染完畢, 頁面上是否真的有一個button元素
    expect(wrapper.contains('button')).toBe(true);
  });

  it('2: button組件點擊時會觸發click事件', () => {
// 依舊是先渲染
    const wrapper = shallowMount(Button);
    // 找到button實例, 這裏的at(0), 相似數組的[0];
    const button = findTestWrapper(wrapper,'button').at(0);
    // 在button身上觸發其click方法
    button.trigger('click');
    // emitted : 返回一個包含由 Wrapper vm 觸發的自定義事件的對象。
    // 也就是監聽是否頁面裏面出發了 this.$emit('click')事件
    // toBeTruthy 這個咱們👆上面講過了
    expect(wrapper.emitted().click).toBeTruthy();
  });

  it('3: 傳入icon參數, 能夠顯示icon組件', () => {
   // shallowMount初始化時, 能夠傳遞參數進去
// 下面的操做你們都懂
    const wrapper = shallowMount(Button,{
      propsData:{
        icon:'cc-up'
      }
    });
    // 找到和這個icon元素
    const icon = findTestWrapper(wrapper,'icon').at(0);
    // 在我傳遞了icon以後, 這個icon組件必須存在
    expect(icon).toBeTruthy();
  });

});

上面的例子裏面提到了一個公共方法我來解釋一下

export const findTestWrapper = (wrapper, tag) => {
    return wrapper.findAll(`[data-test="${tag}"]`);
  };

咱們在書寫代碼的時候, 爲了方便之後的測試, 也會添加一些測試屬性, 好比下面這種

<div data-test='name'>
  {{name}}
</div>

取值:

findTestWrapper(wrapper,'name')

findAll 是 wrapper身上的方法, 與之對應還有find 只找尋一個

輸入框的測試

import { shallowMount } from '@vue/test-utils';
import Input from '../../src/components/Input';
import { findTestWrapper } from '../utils/util';


describe('測試button組件', () => {

  it('1: 能夠渲染出Input組件', () => {
// 這個屬於基礎步驟了
    const wrapper = shallowMount(Input);
    expect(wrapper.contains('input')).toBe(true);
  });

  it('2: 輸入value與顯示的內容相同, 而且修改聯動', () => {
// 測試是否雙向綁定
    const wrapper = shallowMount(Input,{
        propsData:{
            value:'內容1'
        }
    });
    // 取到輸入框實例
    const input = findTestWrapper(wrapper,'input').at(0);
    // element就是直接取到dom了...這個dom也是未dom
    // value能夠模擬的拿出顯示的值
    expect(input.element.value).toBe('內容1')
    // 改變也隨之改變
    wrapper.setProps({ value: '內容2' })
    // 只要一塊兒變了就知足需求
    expect(input.element.value).toBe('內容2')
  });

// 個人輸入框是有清除功能的額
  it('3: 清除內容按鈕有效', () => {
    const wrapper = shallowMount(Input,{
        propsData:{
            value:'內容1',
            clear:true
        }
    });
    // hover 時候纔會出現!!
    // 這是組件的內部觸發條件, setData能夠強行改變組件內部的data數據
    wrapper.setData({
        hovering:true
    })
    const clear = findTestWrapper(wrapper,'clear').at(0);
    // 這裏也講過toBeTruthy能夠判斷是否可轉true
    // 也就是這個定義的實例是否存在
    expect(clear).toBeTruthy();
    // 觸發清除事件
    clear.trigger('click');

    expect(wrapper.emitted().input).toBeTruthy();
  });

  it('4: 傳入icon參數, 能夠顯示icon組件', () => {
    const wrapper = shallowMount(Input,{
      propsData:{
        icon:'cc-up'
      }
    });
    const icon = findTestWrapper(wrapper,'icon').at(0);
    expect(icon).toBeTruthy();
  });

  it('5: 切換type, 出現文本框', () => {
    const wrapper = shallowMount(Input,{
      propsData:{
        type:'textarea'
      }
    });
    const textarea = findTestWrapper(wrapper,'textarea').at(0);
    expect(textarea).toBeTruthy();
  });

});

測試分頁器

import { shallowMount } from '@vue/test-utils';
import Pagination from '../../src/components/Pagination';
import { findTestWrapper } from '../utils/util';

describe('測試分頁器組件', () => {
  it('1: 能夠渲染出分頁器組件', () => {
    const wrapper = shallowMount(Pagination,{
        propsData:{
            pageTotal:5,
            value:1
        }
    });
    // classes  返回 Wrapper DOM 節點的 class。返回 class 名稱的數組。或在提供 class 名的時候返回一個布爾值。這個的意思就是 這個dom的class 是 'cc-pagination'
    expect(wrapper.classes()).toContain('cc-pagination');
  });

  it('2: 傳入1000頁是否顯示1000頁', () => {
    const wrapper = shallowMount(Pagination, {
        propsData:{
            pageTotal:1000,
            pageSize:1000,
            value:1
        }
    });
    const li = findTestWrapper(wrapper, 'item');
    // 這個元素我獲取到了1000個
    expect(li.length).toBe(1000);
  });

  it('3: 點擊第三頁是否跳轉到第三頁', () => {
    const wrapper = shallowMount(Pagination, {
        propsData:{
            pageTotal:10,
            pageSize:10,
            value:1
        }
    });
    wrapper.vm.handlClick(3)
    // 發送事件
    expect(wrapper.emitted().input).toBeTruthy();
    // 發送事件的參數, 注意,是數組的形式
    // 這個事件發送的第一個參數[0]
    expect(wrapper.emitted().input[0]).toEqual([3])
  });
});

寫到這裏你們對測試也應該有了不少本身的想法, 沒試過的小夥伴不妨試一試.

配置

上面沒有提: 開啓實時檢測

"test:unit": "vue-cli-service test:unit --watch",
// 無論改沒改, 全部文件都監控
"test:unit": "vue-cli-service test:unit --watchAll",

end

一套ui組件不寫測試也是說不過去的, 寫的過程也遇到不少不少的坑, 好比說兩個相互以插槽嵌套的組件, 兩個又都有'必傳參數'的限制, vue沒有很好的解決這個問題, 文檔看了很久, 跟個人感受就是有用的東西太少, 沒辦法這就是現狀, 但願測試相關技術支持愈來愈完善吧.

你們均可以一塊兒交流, 共同窗習,共同進步, 早日實現自我價值!!

項目github地址: 連接描述
我的技術博客(ui官網):連接描述

相關文章
相關標籤/搜索