不很久不很久以前
據說某家公司有兩位前端,天天擼bug,為啥嘞?只怪測試MM傾人國,輕語哥哥有bug。✧(๑•̀ㅂ•́)و✧ 可是最近兩位有點犯愁 Σ(っ °Д °;)っ。測試MM有幾次提了緊急bug,都在旁邊鼓勵他們改bug了,可是線上bug重現排查比較麻煩,而且改了後還發現沒改好,惹得測試MM潸然淚下,好生埋汰。怎麼辦呢?
前端君666某天發現了E2E
測試神器cypress
後,暗中偷練神功,改bug越來越6,測試MM每天笑著對他說,666你真6,MM好喜歡呀(๑•́ ₃ •̀๑) 另一位前端君555每天面對堆積如山的bug長吁短嘆,測試MM提完新bug後都不理他了≡ ̄﹏ ̄≡
作為一個追求程式碼永無bug
、順帶跟測試MM溝通產品的有理想的前端 (ง •̀_•́)ง,我覺得有必要學習一下怎麼使用cypress
來進行E2E
測試,以此來提高程式碼質量。那麼我們來看看怎麼入門cypress
測試框架。
cypress三問 - 你是誰
cypress
是在mocha
式API基礎上構建的一套開箱可用的E2E
測試框架,對比其他測試框架,它提供一套自己的最佳實踐方案,無需其他測試工具庫,配置方便簡單但功能異常強大,可以使用webpack
專案配置,還提供了一個強大的GUI
圖形工具。入門簡單,上手方便,怎麼舒服怎麼來呀 (。→‿←。)
cypress
GUI方式的測試使用真實瀏覽器,非GUI方式使用chrome-headless
,不是用模擬方式進行測試,更真實的展現實際環境中的測試過程和結果。
cypress三問 - 你有啥優勢
cypress有幾大自帶的強大功能:
- 自帶GUI工具,想測啥就點啥,還可以檢視整個測試過程,想錄屏還可以錄屏喲(錄屏可以發給測試MM看,保準她說哥哥真厲害喲。 一般人我不告訴他๑乛◡乛๑)
- 測試的每一步都有snapshot,可以通過GUI工具檢視每個過程的頁面狀態,不是截圖而是真是的頁面DOM環境喲!
- 自帶資料mock和請求攔截機制,還原線上資料引起的bug別提有多輕鬆了
- 和wepbakc配置,實現無論修改測試檔案還是被測試程式碼都可以自動重測
- 小Tips:可以給測試用例加上
only
或者skip
來避免重測測試檔案裡的所有用例:it.only('只測試這個喲); it.skip('不要測這個');
- 小Tips:可以給測試用例加上
cypress三問 - 怎麼用
安裝
yarn add cypress
或者npm install cypress
- 安裝完畢後,
./node_modules/.bin/cypress install
安裝cypress環境(包括GUI工具)
配置
- package.json: 配置GUI和非GUI(terminal)兩種方式來執行cypress
"scripts": {
"cypress": "cypress run",
"cypress-gui": "cypress open",
複製程式碼
⚠️ 配置好後 先執行 yarn cypress[-gui]
或者 npm run cypress[-gui]
(中括號意思是可選)來初始化cypress
,生成預設配置和目錄
- cypress.json(與package.json同級目錄): cypress提供比較靈活的配置,可以根據自己需要定製行為,以下列一下我對一個專案的配置
{
"baseUrl": "http://localhost:8080", // 本地開發服務地址(webpack-dev-server)
"integrationFolder": "src", // 自定義"src"為測試檔案根目錄,預設是"cypress/integration"
"testFiles": "**/*.cypress.spec.js", // 自定義測試檔案的匹配正則,預設是"**/*.*",即所有檔案
"videoRecording": false, // 關閉錄屏功能, 如果開啟錄屏功能,記得將"cypress/screenshots"目錄加入".gitignore",防止不小心將錄屏加到git中
"viewportHeight": 800, // 設定測試環境的頁面檢視的高度
"viewportWidth": 1600 // 設定測試環境的頁面檢視的寬度
}
複製程式碼
- cypress/plugins/index.js: cypress執行環境配置,可以用來配置webpack等。以下是配置webpack別名範例。預設這裡不需要配置。
// 參考官方例子地址 https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/preprocessors__typescript-webpack/cypress/plugins/index.js
const wp = require("@cypress/webpack-preprocessor");
const path = require('path');
function resolve(dir) {
return path.join(__dirname, "../..", dir);
}
module.exports = on => {
const options = {
webpackOptions: {
resolve: {
alias: {
"@": resolve("src"),
cypress: resolve("cypress")
}
}
}
};
on("file:preprocessor", wp(options));
};
複製程式碼
萬事俱備,測測測
- 簡單的一個例子
describe('測試頁面包含某元素', () => {
it('有云 "前端哥哥們真帥,前端妹妹們真漂亮"', () => {
cy.contains("前端哥哥們真帥,前端妹妹們真漂亮");
});
it('要有一個連結', () => {
cy.get('a').should('have.length', 1);
});
it('不存在class含有abc的元素', () => {
cy.get('.abc').should('have.length', 0);
});
});
複製程式碼
- 互動的例子
describe('一起動', () => {
it('獲取輸入框,輸入文字並按enter鍵', () => {
const text = 'not exist';
// type api用法: https://docs.cypress.io/api/commands/type.html#Usage
cy.get('input').type(`${text}{enter}`);
});
it('點選按鈕', () => {
cy.get('button').click();
});
});
複製程式碼
- 網路請求mock例子
Tip1: cy.route的路徑匹配是嚴格的,所以要注意是否需要加萬用字元。如
cy.route('/api/search', [])
不會攔截/api/search?keyword=abc
,只會攔截/api/search
。
Tip2: cy.route的
method
要注意,預設是GET
,cy.route('/api/posts')
和cy.route('POST', '/api/posts')
是不一樣的。
describe('要啥給啥', () => {
beforeEach(() => {
cy.server(); // 一定要在 cy.route 前呼叫
cy
.fixture('/posts/list.json') // 我們在 cypress/fixtures 內建立mock用的資料
.as('postsData'); // 給 mock 資料取別名,以後 cy.route 使用
cy
.route('/api/posts', '@postsData')
.as('getPostsRoute'); // 給請求取別名,以供 cy.wait 使用
})
it('進入列表頁,攔截列表請求介面', () => {
cy.wait('@getPostsRoute'); // 等待被攔截的介面請求完成
cy.get('.post').should('have.length', 10); // 要有10條資料被渲染到頁面上
});
})
複製程式碼
- 實際場景例子: 結合上面所有姿勢,我們現在測試搜尋頁面的搜尋、操作結果
describe('test search page', () => {
// 幾個 route 路徑變數
const searchRoutePath = '/api/items/activities?query=*';
const deleteActivityRoutePath = '/api/activities/*/items/batch?num_iids[]=*';
const undoActivityRoutePath = '/api/activities/*/items/undo';
function search(keyword) {
// 將搜尋行為和等待搜尋返回封裝起來
cy
.fixture('items/activities.json')
// 處理mock資料,只返回符合搜尋結構的資料
.then(data => data.filter(item => item.title.indexOf(keyword) !== -1))
.as('searchResult');
cy.server();
cy.route(searchRoutePath, '@searchResult').as('searchRoute');
const input = cy.get('input');
input.clear(); // 清空輸入框內文字
input.type(`${keyword}{enter}`);
cy.wait('@searchRoute');
}
before(() => {
// 進行所有測試前,先訪問搜尋頁
cy.visit('/activities/search');
});
it('should show no data tip when search result is empty', () => {
const text = 'not exist';
search(text);
cy.contains(`沒有找到關於 ${text} 的結果`);
});
it('should remove activity from list when clean successful', () => {
search('成功');
cy
.route('delete', deleteActivityRoutePath, {
success: 0,
fail: 0,
waiting: 0,
})
.as('deleteActivityResponse');
// within是讓cy執行的context保持在'.activities-search'這個dom節點內
// 預設cy的執行是以上一個cy命令結果作為context
// 如 "cy.get('a'); cy.get('span')",cy會在上一個命令找到的'a'標籤中查詢'span'
cy.get('.activities-search').within(() => {
const items = cy.get('.result-item');
items.should('have.length', 1);
const applyList = items.get('.apply-list');
applyList.should('not.be.visible'); // 每個資料項內詳細內容區域是隱藏的
const toggleBtn = items.get('.item-apply-count');
toggleBtn.click(); // 點選顯示詳細內容區
applyList.should('be.visible');
applyList.children().should('have.length', 1); // 詳細內容區內資料只有1條
const cleanBtn = cy.contains('退出');
cleanBtn.click(); // 點選詳細內容區裡的“退出”按鈕
cy.wait('@deleteActivityResponse'); // 等待“退出”請求返回
cy.get('.apply-list').should('be', null); // 退出成功後,詳細內容區資料減1,即空
});
});
});
複製程式碼
幾個必讀文件
- network-requests : https://docs.cypress.io/guides/guides/network-requests.html
- assertions : https://docs.cypress.io/guides/references/assertions.html
- recipes 示例 : https://docs.cypress.io/examples/examples/recipes.html
- code completion 程式碼提示: https://docs.cypress.io/guides/tooling/intelligent-code-completion.html
關於測試覆蓋率
目前cypress
沒有內建測試覆蓋率統計功能,github上有專門的issue在跟蹤這個,後續應該會有。issue上也有幾個臨時方案,目前我傾向使用chrome
自帶的來檢視。在GUI開啟的測試的瀏覽器中開啟devtools
,切到Sources
, 按下cmd+shift+p
(windows使用者按ctrl+shift+p
),輸入coverage
,選擇重新重新整理並統計程式碼執行覆蓋率。
那麼,high起來
為了高(撩)質(測)量(試)代(M)碼(M),high起來。喜歡前端MM的可以手把手教起來了 (¬_¬)
本文章首發於本人公眾號:楓之葉。有興趣的可以長按下方二維碼關注。^v^