撩測試MM神器cypress使用入門

楓~風發表於2018-04-19

不很久不很久以前

據說某家公司有兩位前端,天天擼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圖形工具。入門簡單,上手方便,怎麼舒服怎麼來呀 (。→‿←。)

cypressGUI方式的測試使用真實瀏覽器,非GUI方式使用chrome-headless,不是用模擬方式進行測試,更真實的展現實際環境中的測試過程和結果。

cypress三問 - 你有啥優勢

cypress有幾大自帶的強大功能:

  • 自帶GUI工具,想測啥就點啥,還可以檢視整個測試過程,想錄屏還可以錄屏喲(錄屏可以發給測試MM看,保準她說哥哥真厲害喲。 一般人我不告訴他๑乛◡乛๑)
  • 測試的每一步都有snapshot,可以通過GUI工具檢視每個過程的頁面狀態,不是截圖而是真是的頁面DOM環境喲!
  • 自帶資料mock和請求攔截機制,還原線上資料引起的bug別提有多輕鬆了
  • 和wepbakc配置,實現無論修改測試檔案還是被測試程式碼都可以自動重測
    • 小Tips:可以給測試用例加上only或者skip來避免重測測試檔案裡的所有用例: it.only('只測試這個喲); it.skip('不要測這個');

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要注意,預設是GETcy.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,即空
        });
    });
});
複製程式碼

幾個必讀文件

關於測試覆蓋率

目前cypress沒有內建測試覆蓋率統計功能,github上有專門的issue在跟蹤這個,後續應該會有。issue上也有幾個臨時方案,目前我傾向使用chrome自帶的來檢視。在GUI開啟的測試的瀏覽器中開啟devtools,切到Sources, 按下cmd+shift+p(windows使用者按ctrl+shift+p),輸入coverage,選擇重新重新整理並統計程式碼執行覆蓋率。

untitled4

那麼,high起來

為了高(撩)質(測)量(試)代(M)碼(M),high起來。喜歡前端MM的可以手把手教起來了 (¬_¬)


本文章首發於本人公眾號:楓之葉。有興趣的可以長按下方二維碼關注。^v^

撩測試MM神器cypress使用入門

相關文章