前端需要寫自動化測試嗎?那又該怎麼寫呢?【建議收藏】

語言: CN / TW / HK

為什麼需要寫前端自動化

大部分企業為了追求開發效率,所以並沒有去強制要求員工寫前端自動化測試的代碼。另一部分企業則會要求前端開發額外寫前端自動化測試。那麼寫和不寫到底有哪些區別呢?

不寫前端自動化測試代碼:

  • 修改某個模塊功能時,其它模塊也受影響,很難快速定位bug
  • 多人開發代碼越來越難以維護
  • 不方便迭代,代碼重構困難
  • 代碼質量差,參差不齊

增加自動化測試後:

  • 我們為核心功能編寫測試後可以保障項目的可靠性
  • 強迫開發者編寫更容易被測試的代碼,提高代碼質量
  • 編寫的測試有文檔的作用,方便維護
  • 開發速度有所變慢,因為要多寫一份測試代碼(手動滑稽)

實際上僅僅就為了可靠性、碼質量、可維護性是完全值得你去寫前端自動化的。作為一名前端開發人員掌握自動化測試技術是必不可少的,就算你工作中不用,也可以放在你的簡歷中,它是一道靚麗的風景線。

測試介紹

測試在工作中分為以下兩個大塊:

黑盒測試和白盒測試

  • 黑盒測試一般也被稱為功能測試,黑盒測試要求測試人員將程序看作一個整體,不考慮其內部結構和特性,只是按照期望驗證程序是否能正常工作。(薪資12k見頂)
  • 白盒測試是基於代碼本身的測試,一般指對代碼邏輯結構的測試。(薪資10k起)

測試分類

單元測試(Unit Testing)

單元測試是指對程序中最小可測試單元進行的測試,例如測試一個函數、一個模塊、一個組件...

集成測試(Integration Testing)

將已測試過的單元測試函數進行組合集成暴露出的高層函數或類的封裝,對這些函數或類進行的測試。

端到端測試(E2E Testing)

打開應用程序模擬輸入,檢查功能以及界面是否正確。

不過作為一名前端,我們需要寫哪些測試呢?答案是:

  • 單元測試
  • 端到端測試(E2E)

接下來就帶你們來實踐一下。

新建項目

我們新建個vue2的項目,選擇下面這些 注意我們到了選擇單元測試框架這一步的時候,選擇jest: 然後端對端我們選擇: 這樣繼續安裝就好了,直到項目建好。

單元測試

單元測試(unit testing),是指對軟件中的最⼩小可測試單元進⾏行行檢查和驗證。例如一個函數。 單測針對組件 或者函數 或者模塊(開發人員知道具體邏輯)

前面建項目的時候提到了jest,實際上在vue中,推薦⽤ Mocha+chai 或者jest來做單元測試,但咱們這邊使⽤用 jest 來演示,兩者語法基本⼀致。

測試某個函數

在src目錄下建立utils目錄,再在utils下建立index.js,並寫個add函數導出:

export function add(x,y) {
    return x + y
}

在tests下的unit下的example.spec.js中修改(當然也可以新建一個*.spec.js,固定格式的文件)

// import { shallowMount } from '@vue/test-utils'
// import HelloWorld from '@/components/HelloWorld.vue'

// describe('HelloWorld.vue', () => {
//   it('renders props.msg when passed', () => {
//     const msg = 'new message'
//     const wrapper = shallowMount(HelloWorld, {
//       propsData: { msg }
//     })
//     expect(wrapper.text()).toMatch(msg)
//   })
// })

import { add } from "@/utils/index.js";
describe("測試加法函數", () => {
  //測試代碼可讀性最好
  //分組
  it("一個具體的功能測試,測測試數字相加", () => {
    expect(add(1, 2)).toBe(3);
  });
  it("一個具體的功能測試,測測試數字和字符串相加", () => {
    expect(add("a", 2)).toBe("a2");
  });
  it("一個具體的功能測試,測測試數字字符串相加", () => {
    expect(add("1", 2)).toBe(3);
  });
});

執行:

npm run test:unit

顯然"1" + 2 不是3,所以測試是報了錯的。

這個案例我們就用到了4個api。

api介紹

  • describe : 定義⼀一個測試套件
  • it :定義⼀一個測試⽤用例例
  • expect :斷⾔言的判斷條件
  • toBe :斷⾔言的⽐比較結果

測試vue組件

components下新建 zhifeiji.vue 文件:

<template>
  <div>
    <span>{{ msg }}</span>
    <span>{{ msg1 }}</span>
    <button class="btn" @click="changeMsg">點我</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: "vue test",
      msg1: "你好",
    };
  },
  created() {
    this.msg = "aftermounted";
  },
  mounted() {
    this.msg1 = "測試下vue組件";
  },
  methods: {
    changeMsg() {
      this.msg = "click over";
    },
  },
};
</script>

views目錄下的home組件中引入:

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png" />
    <zhifeiji_comp></zhifeiji_comp>
  </div>
</template>

<script>
// @ is an alias to /src
// import HelloWorld from "@/components/HelloWorld.vue";
import zhifeiji_comp from "@/components/zhifeiji.vue";

export default {
  name: "home",
  components: {
    // HelloWorld,
    zhifeiji_comp,
  },
};
</script>

在tests目錄下的unit目錄下建立 zhifeiji.spec.js:

import Vue from "vue";
import zhifeijiComp from "@/components/zhifeiji.vue";
import { mount } from "@vue/test-utils";

describe("測試zhifeiji組件", () => {
  it("測試初始化的data", () => {
    expect(zhifeijiComp.data().msg).toBe("vue test");
  });
  //created和mounted裏數據測試都是一樣的
  it("測試新建完畢後,create生命週期後的數據", () => {
    //created
    let vm = new Vue(zhifeijiComp).$mount();
    expect(vm.msg).toBe("aftermounted");
  });
  it("測試新建完畢後,create生命週期後的數據", () => {
    //mounted
    let vm = new Vue(zhifeijiComp).$mount();
    expect(vm.msg1).toBe("測試下vue組件");
  });
  //點擊事件測試
  it("測試點擊後,msg的改變", () => {
    // $mount處理不了用户交互,所以我們要用到vue官方推薦的@vue/test-utils
    let wrapper = mount(zhifeijiComp);
    expect(wrapper.vm.msg).toBe("aftermounted");
    //點擊一下
    wrapper.find(".btn").trigger("click");
    expect(wrapper.vm.msg).toBe("click over");
  });
});

如果測試用户交互的話,需要用到官方推薦的@vue/test-utils,執行cnpm i @vue/test-utils --save,相關文檔在vue官網

在 @vue/test-utils 中引入mount替代vue的 $mount 的是因為 $mount 的是虛擬的,存在虛擬內存中,處理不了dom,所以用mount(對不對我不知道,這句話僅供參考)

到這裏為止的話,我這個vue組件的測試,應該是通過的才對,執行npm run test:unit驗證一下:

測試覆蓋率

jest⾃自帶覆蓋率,如果⽤用的 mocha,需要使⽤用 istanbul來統計覆蓋率。(所以我推薦jest)

npm run test:unit

這個明顯是不通過的,需要將途中的3改為正確的‘12’。 改完後再次執行命令。 這麼一來測試是通過了,但是好像無法直觀看到覆蓋率,所以我們來對package.json中的jest進行下修改,在下圖的位置中: 改為:

"jest": {
    "collectCoverage": true,
    "collectCoverageFrom": ["src/**/*.{js,vue}"],
    "moduleFileExtensions": [
      "js",
      "json",
      "vue"
    ],
    "transform": {
      "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
      ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
    },
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    },
    "snapshotSerializers": [
      "<rootDir>/node_modules/jest-serializer-vue"
    ],
    "testURL": "http://localhost"
  }

倘若有個jest.config.js那麼就在 moduleFileExtensions 上一行加入,這個文件是對jest的配置(沒有不必改):

"collectCoverage": true,
"collectCoverageFrom": ["src/**/*.{js,vue}"]

改好後,我們再來執行下npm run test:unit命令, 到此,我們就能清晰看見測試的一個覆蓋情況!這個覆蓋率會生成一個報告,生成的報告文件在coverage目錄下,打開可以看到這麼個鬼東西: 這麼一來,測試代碼覆蓋情況就一覽無遺了。

這個測試報告可以很精確的看到我們哪些沒測試,哪些測試了。

我在zhifeiji.vue里加點東西不測試,來看看結果:

<template>
  <div>
    <span id="msg">{{ msg }}</span>
    <span>{{ msg1 }}</span>
    <button class="btn" @click="changeMsg">點我</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: "vue test",
      msg1: "你好",
      msg2:'這個不測試'
    };
  },
  created() {
    this.msg = "aftermounted";
    this.msg2 = '不測試扣了我100塊';
  },
  mounted() {
    this.msg1 = "測試下vue組件";
  },
  methods: {
    changeMsg() {
        if(this.msg === '1'){
            this.msg = "click false";
        }else{
            this.msg = "click over";
        }
      
    },
  },
};
</script>

上面代碼中msg2有關的都是沒測試的,執行下測試命令看報告: 紅框部分可見着這次zhifeiji.vue覆蓋率沒有100%了,上main藍色的我們可以點擊進去。 一步步點進來,會來到被測試的代碼文件。

如何測試不通過就阻止代碼git提交

安裝husky:

cnpm i husky –save
# yarn、cnpm、npm 隨意

配置husky,在package.json配置:

"husky": {
    "hooks": {
      "pre-commit": "npm test",
      "pre-push": "npm test",
      "...": "..."
    }
}

當然這是文檔上的案例,我們改下:

"husky": {
    "hooks": {
      "pre-commit": "npm run test:unit"
    }
}

代表再提交(commit)前先執行npm run test:unit。

驗證 修改測試代碼文件zhifeiji.spec.js: 由:

it("測試新建完畢後,create生命週期後的數據", () => {
    //created
    let vm = new Vue(zhifeijiComp).$mount();
    expect(vm.msg).toBe("aftermounted");
});

改為:

it('測試新建完畢後,create生命週期後的數據',() => {
	//created
    let vm = new Vue(zhifeijiComp).$mount()
    expect(vm.msg).toBe('aftermounted1')
})

這樣測試肯定是不通過的。 這時,測試不通過,我們再來提交代碼,add後再commit: 可見commit是失敗了,沒有通過!

Tip:如果你們這麼配置husky不生效,請降低husky版本或者使用新的husky的方式,husky在6.0.0版本開始就使用了新的配置方法,具體看此文:https://blog.csdn.net/MrWeb/article/details/119878688

這時我們如果想讓他通過的話就得改代碼了,讓其正確了才能提交,這樣才是安全可靠的一份代碼。

修改操作: 1.註釋掉tests目錄下e2e中的test.js,e2e我們後面講 2.example.spec.js中的3修改為'12' 3.zhifeiji.spec.js的aftermounted1恢復為aftermounted

經過這三步操作,我們就算是解決了測試的報錯,然後commit: 代碼提交成功!

E2E測試

e2e針對應用,站在測試人員的角度,沒有什麼mount 加載,只有按鈕 頁面,輸入框,文本等

借⽤瀏覽器器的能力,站在⽤户測試⼈員的角度,輸⼊框,點擊按鈕等,完全模擬用户,這個和具體的框架關係不大,完全模擬瀏覽器行為

將views下的Home組件的:

<HelloWorld msg="Welcome to Your Vue.js App"></HelloWorld>

相關代碼恢復。 看看tests目錄下e2e目錄下的spec下的test.js,有這麼一段:

// https://docs.cypress.io/api/introduction/api.html

describe('My First Test', () => {
  it('Visits the app root url', () => {
    // cy.visit('/')
    // cy.contains('h1', 'Welcome to Your Vue.js App')
  })
})

將cy註釋的那塊恢復。

describe('My First Test', () => {
  it('Visits the app root url', () => {
    cy.visit('/')
    cy.contains('h1', 'Welcome to Your Vue.js App')
  })
})

這是測試代碼,如果我們不做前面的恢復操作的話,測試肯定是不通過的

執行npm run test:e2e,進行測試

項目會啟動,並且會彈出一個有ok的彈窗,我們直接確定,還會有測試文件的js,選擇對應的測試文件點進去: 這樣代表測試通過了!

再來測試一個about頁:

// https://docs.cypress.io/api/introduction/api.html

describe('My First Test', () => {
  it('Visits the app root url', () => {
    cy.visit('/')
    cy.contains('h1', 'Welcome to Your Vue.js App')
  })
  it('測試about頁', () => {
    //訪問about
    cy.visit('about')
    cy.contains('h1', 'This is an about page')
  })
})

Tip:about頁面測試不通過的,請將路由模式改為history或者將測試文件test.js中的cy.visit('about')改為cy.visit('#/about')

後測的about,就最後打開了about頁,也停留在了about頁面

這些都是頁面某個元素的文本的測試,那麼我們再來個交互試試,在我們的zhifeiji組件上有個點擊事件,我們來試試:

describe('My First Test', () => {
  it('Visits the app root url', () => {
    cy.visit('/')
    cy.contains('h1', 'Welcome to Your Vue.js App')
  })
  it('測試about頁', () => {
    //訪問about
    cy.visit('#/about')
    cy.contains('h1', 'This is an about page')
  })
  it('zhifeiji組件',() => {
    //訪問根目錄
    cy.visit('/')
    cy.contains('#msg','aftermounted')
    cy.get('button').click()//點擊button元素
    cy.contains('#msg','click over')
  })
})

左邊那一條條可以點擊,點擊不同的項,也會進入不同的事件狀態,例如我點了CONTAINS,文本變成了click over。

題外話:測試頁面前後差異,或者兩個頁面的差異可以用page-monitor;地址:https://github.com/fouber/page-monitor

好了,今天的前端自動化測試就講到這裏。

熱門推薦:

全棧開發微信公眾號

typescript入門

vue中使用JSX

前端基本知識