聽說某家公司有兩位前端,每天擼bug,爲啥嘞?只怪測試MM傾人國,輕語哥哥有bug。✧(๑•̀ㅂ•́)و✧ 但是最近兩位有點犯愁 Σ(っ °Д °;)っ。測試MM有幾回提了緊急bug,都在旁邊鼓勵他們改bug了,但是線上bug重現排查比較麻煩,並且改了後還發現沒改好,惹得測試MM潸然淚下,好生埋汰。怎麼辦呢?javascript
前端君666某天發現了E2E
測試神器cypress
後,暗中偷練神功,改bug愈來愈6,測試MM天天笑着對他說,666你真6,MM好喜歡呀(๑•́ ₃ •̀๑) 另外一位前端君555天天面對堆積如山的bug唉聲嘆氣,測試MM提完新bug後都不理他了≡ ̄﹏ ̄≡html
做爲一個追求代碼永無bug
、順帶跟測試MM溝通產品的有理想的前端 (ง •̀_•́)ง,我以爲有必要學習一下怎麼使用cypress
來進行E2E
測試,以此來提升代碼質量。那麼咱們來看看怎麼入門cypress
測試框架。前端
cypress
是在mocha
式API基礎上構建的一套開箱可用的E2E
測試框架,對比其餘測試框架,它提供一套本身的最佳實踐方案,無需其餘測試工具庫,配置方便簡單但功能異常強大,可使用webpack
項目配置,還提供了一個強大的GUI
圖形工具。入門簡單,上手方便,怎麼舒服怎麼來呀 (。→‿←。)java
cypress
GUI方式的測試使用真實瀏覽器,非GUI方式使用chrome-headless
,不是用模擬方式進行測試,更真實的展示實際環境中的測試過程和結果。node
cypress有幾大自帶的強大功能:webpack
和wepbakc配置,實現不管修改測試文件仍是被測試代碼均可以自動重測git
only
或者skip
來避免重測測試文件裏的全部用例: it.only('只測試這個喲); it.skip('不要測這個');
yarn add cypress
或者 npm install cypress
./node_modules/.bin/cypress install
安裝cypress環境(包括GUI工具)"scripts": { "cypress": "cypress run", "cypress-gui": "cypress open",
⚠️ 配置好後 先運行 yarn cypress[-gui]
或者 npm run cypress[-gui]
(中括號意思是可選)來初始化cypress
,__生成默認配置和目錄__github
{ "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 // 設置測試環境的頁面視圖的寬度 }
// 參考官方例子地址 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(); }); });
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')
是不同的。web
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,即空 }); }); });
目前cypress
沒有內置測試覆蓋率統計功能,github上有專門的issue在跟蹤這個,後續應該會有。issue上也有幾個臨時方案,目前我傾向使用chrome
自帶的來查看。在GUI打開的測試的瀏覽器中打開devtools
,切到Sources
, 按下cmd+shift+p
(windows用戶按ctrl+shift+p
),輸入coverage
,選擇從新刷新並統計代碼執行覆蓋率。chrome
爲了高(撩)質(測)量(試)代(M)碼(M),high起來。喜歡前端MM的能夠手把手教起來了 (¬_¬)
本文章首發於本人公衆號:楓之葉。有興趣的能夠長按下方二維碼關注。^v^