国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

前端自動化測試(二)Vue Test Utils + Jest

這篇具有很好參考價(jià)值的文章主要介紹了前端自動化測試(二)Vue Test Utils + Jest。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

1、 概述

1、 為什么需要自動化測試?

項(xiàng)目會從快速迭代走向以維護(hù)為主的狀態(tài),因此引入自動化測試能有效減少人工維成本 。

自動化的收益 = 迭代次數(shù) * 全手動執(zhí)行成本 - 首次自動化成本 - 維護(hù)次數(shù) * 維護(hù)成本

對于自動化測試,相對于發(fā)現(xiàn)未知問題,更傾向于避免可能的問題。

2、 分類

(1) 單元測試

單元測試(unit testing),是指對軟件中的最小可測試單元進(jìn)行檢查和驗(yàn)證,通常是針對函數(shù)模塊、對象進(jìn)行測試,但在前端應(yīng)用中組件也是被測單元,對于代碼中多個組件共用的工具類庫、多個組件共用的子組件應(yīng)盡可能提高覆蓋率。
特點(diǎn):

  • 單元測試執(zhí)行速度很快;
  • 應(yīng)該避免依賴性問題,如不存取數(shù)據(jù)庫,不訪問網(wǎng)絡(luò)等,而是使用工具虛擬出運(yùn)行環(huán)境;
  • 由于單元測試是獨(dú)立的,因此無法保證多個單元一起運(yùn)行時的正確性。

意義:

  • 通過用例確保模塊的功能,不至于在迭代過程中產(chǎn)生 bug ;
  • 保證代碼重構(gòu)的安全性,測試用例能給你多變的代碼結(jié)構(gòu)一個定心丸;
  • 如果模塊邏輯越來越復(fù)雜,通過單測用例,也能比較快地了解模塊的功能 ;
  • 提高代碼質(zhì)量,使得代碼設(shè)計(jì)的與外部模塊更加解耦。

(2) UI測試

TODO文章來源地址http://www.zghlxwxcb.cn/news/detail-717832.html

(3) E2E測試

TODO

3、 測試思想

TDD:Test-Driven Development(測試驅(qū)動開發(fā))

TDD 要求在編寫某個功能的代碼之前先編寫測試代碼,然后只編寫使測試通過的功代碼,通過測試來推動整個開發(fā)的進(jìn)行。

BDD:Behavior-Driven Development(行為驅(qū)動開發(fā))

BDD 可以讓項(xiàng)目成員(甚至是不懂編程的)使用自然語言來描述系統(tǒng)功能和業(yè)務(wù)輯,從而根據(jù)這些描述步驟進(jìn)行系統(tǒng)自動化的測試。

2、 技術(shù)選型

1、 單元測試

框架對比:

框架 斷言 仿真 快照 異步測試 覆蓋率
Mocha 默認(rèn)不支持 默認(rèn)不支持 默認(rèn)不支持 友好 不支持
Ava 默認(rèn)支持 不支持 默認(rèn)支持 友好 不支持
Jasmine 默認(rèn)支持 默認(rèn)支持 默認(rèn)支持 不友好
Jest 默認(rèn)支持 默認(rèn)支持 默認(rèn)支持 友好 默認(rèn)支持
Karma 不支持 不支持 不支持 不支持

經(jīng)過對比,主要在Jest和Mocha間進(jìn)行選擇,同樣Vue Test Utils ( Vue.js 官方的元測試實(shí)用工具庫)中也主要介紹了該兩種框架的使用方式。
Jest默認(rèn)支持所需多種場景,可通過較少配置滿足所需功能,開箱即用,同時我們通希望與Jenkins完成配合,如設(shè)置某項(xiàng)指標(biāo)覆蓋率低于80%則不進(jìn)行build,不通過Jenkins校驗(yàn),Jest可以簡單配置coverageThreshold進(jìn)行實(shí)現(xiàn),除此以外也可以單獨(dú)為某個模塊配置報(bào)錯閾值,提供更靈活的覆蓋率選擇。

// jest.config.js
module.exports = {
    coverageThreshold: {
      // 覆蓋結(jié)果的最低閾值設(shè)置,如果未達(dá)到閾值,jest將返回失敗。
      global: {
        branches: 60,
        functions: 80,
        lines: 80,
        statements: 80,
      },
    }
}

綜上所述,前端單元測試采用Jest框架+ Vue Test Utils完成單元測試,并對工具未覆蓋的常用方法進(jìn)行封裝。

使用方式:

  • 斷言:所謂斷言,就是判斷源碼的實(shí)際執(zhí)行結(jié)果與預(yù)期結(jié)果是否一致,如果不一致就拋出一個錯誤,通常斷言庫為expect斷言風(fēng)格(BDD),更接近自然語言;
  • 仿真:即通常所說的mock功能,當(dāng)需要測試的單元需要外部模塊時,同時這些模塊具有不可控、實(shí)現(xiàn)成本高等原因時,此時采用mock,例如模擬http請求;
  • 快照:快照測試通常是對UI組件渲染結(jié)果的測試,而在jest中,快照測試是保存渲染組件的標(biāo)記,從而達(dá)到快照文件體積小,測試速度快的目的;
  • 異步測試:通常異步測試進(jìn)行http請求的異步獲取模擬,支持promise,async/await等語法,能夠簡單進(jìn)行異步模擬;
  • 覆蓋率:覆蓋率通常通過以下指標(biāo)進(jìn)行統(tǒng)計(jì):
    • %stmts是語句覆蓋率(statement coverage):是不是每個語句都執(zhí)行了?
    • %Branch分支覆蓋率(branch coverage):是不是每個if代碼塊都執(zhí)行了?
    • %Funcs函數(shù)覆蓋率(function coverage):是不是每個函數(shù)都調(diào)用了?
    • %Lines行覆蓋率(line coverage):是不是每一行都執(zhí)行了?

我們至少需要測試框架(運(yùn)行測試的工具),斷言庫來保證單元測試的正常執(zhí)行。在業(yè)務(wù)場景中,Api請求等異步場景也希望框架擁有異步測試能力,同時希望框架支持生成覆蓋率報(bào)告。

2、 UI測試

TODO

3、 E2E測試

TODO

3、 單元測試

1、 依賴安裝

vue add @vue/cli-plugin-unit-jest

通過該命令將自動安裝Jest和Vue Test Utils等所需工具

依賴安裝完成后我們在package.json文件應(yīng)該能看到以下依賴:
vue框架自動化測試,jest,vue,JavaScript,前端,vue.js,單元測試

項(xiàng)目自動生成如下文件:
vue框架自動化測試,jest,vue,JavaScript,前端,vue.js,單元測試

tests目錄是自動化測試的工作區(qū),可mock方法mock請求、預(yù)置配置加入工具方法、編寫單元測試等。
jest.config.js文件用于配置jest的測試環(huán)境、es6語法轉(zhuǎn)換需要檢測的文件類型、css預(yù)處理、覆蓋率報(bào)告等。

2、 Jest配置

// jest.config.js
module.exports = {
  preset: "@vue/cli-plugin-unit-jest",
  verbose: true, // 多于一個測試文件運(yùn)行時展示每個測試用例測試通過情況
  bail: true, // 參數(shù)指定只要有一個測試用例沒有通過,就停止執(zhí)行后面的測試用例
  testEnvironment: 'jsdom', // 測試環(huán)境,jsdom可以在Node虛擬瀏覽器環(huán)境運(yùn)行測試
  moduleFileExtensions: [ // 需要檢測測的文件類型
    'js',
    'jsx',
    'json',
    // tell Jest to handle *.vue files
    'vue'
  ],
  transform: { // 預(yù)處理器配置,匹配的文件要經(jīng)過轉(zhuǎn)譯才能被識別,否則會報(bào)錯
    '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|avif)$':
    require.resolve('jest-transform-stub'),
    '^.+\\.jsx?$': require.resolve('babel-jest')
  },
  transformIgnorePatterns: ['/node_modules/'], // 轉(zhuǎn)譯時忽略 node_modules
  moduleNameMapper: { // 從正則表達(dá)式到模塊名稱的映射,和webpack的alisa類似
    "\\.(css|less|scss|sass)$": "<rootDir>/tests/unit/StyleMock.js",
  },
  snapshotSerializers: [ // Jest在快照測試中使用的快照序列化程序模塊的路徑列表
    'jest-serializer-vue'
  ],
  testMatch: [ // Jest用于檢測測試的文件,可以用正則去匹配
    '**/tests/unit/**/*.spec.[jt]s?(x)',
    '**/__tests__/*.[jt]s?(x)'
  ],
  collectCoverage: true, // 覆蓋率報(bào)告,運(yùn)行測試命令后終端會展示報(bào)告結(jié)果
  collectCoverageFrom: [ // 需要進(jìn)行收集覆蓋率的文件,會依次進(jìn)行執(zhí)行符合的文件
    'src/views/**/*.{js,vue}',
    '!**/node_modules * '
  ],
  coverageDirectory: "<rootDir>/tests/unit/coverage", // Jest輸出覆蓋信息文件的目錄,運(yùn)行測試命令會自動生成如下路徑的coverage文件
  coverageThreshold: { // 覆蓋結(jié)果的最低閾值設(shè)置,如果未達(dá)到閾值,jest將返回失敗
    global: {
      branches: 60,
      functions: 80,
      lines: 80,
      statements: 80,
    },
    "src/views/materialManage/materialList/index.vue": {
      branches: 100,
      functions: 100,
      lines: 100,
      statements: 100,
    },
  },
  setupFiles: ["<rootDir>/tests/unit/setup/main.setup.js"] // 環(huán)境預(yù)置配置文件入口
};
  • preset(@vue/cli-plugin-unit-jest):提供了jest默認(rèn)配置,可通過路徑node_modules/@vue/cli-plugin-unit-jest/presets/default/jest-preset.js找到該默認(rèn)配置;

  • verbose:多于一個測試文件運(yùn)行時展示每個測試用例測試通過情況,默認(rèn)多于一個測試文件時不展示;

  • bail:默認(rèn)Jest會運(yùn)行所有測試用例并將全部錯誤輸出至控制臺,bail可設(shè)置當(dāng)n個用例不通過后停止測試,當(dāng)設(shè)置為true時等同于1,在后續(xù)與Jenkins配合時可將其配置為true減少不必要的資源消耗,默認(rèn)值為0;

  • testEnvironment(jsdom):jsdom可以讓js在node環(huán)境運(yùn)行,是自動化測試必要條件;

  • moduleFileExtensions:jest需要檢測測的文件類型;

  • transform:預(yù)處理器配置,匹配的文件要經(jīng)過轉(zhuǎn)譯才能被識別,否則會報(bào)錯;

  • transformIgnorePatterns:匹配所有源文件路徑的regexp模式字符串?dāng)?shù)組,匹配的文件將跳過轉(zhuǎn)換;

  • moduleNameMapper:從正則表達(dá)式到模塊名稱的映射,支持源代碼中相同的@別名,與vue.config.js中chainWebpack的alias相對應(yīng);

  • snapshotSerializers:Jest在快照測試中使用的快照序列化程序模塊的路徑列表;

  • testMatch:當(dāng)只需要進(jìn)行某個目錄下的單元測試腳本執(zhí)行時可以進(jìn)行該配置,例如示例中僅執(zhí)行unit下的測試腳本,默認(rèn)直接注釋該行即可;

  • collectCoverage:是否生成覆蓋率報(bào)告,將會為每個測試范圍內(nèi)的文件收集并統(tǒng)計(jì)覆蓋率,生成html可視的測試報(bào)告,但會顯著降低單元測試運(yùn)行效率,通常設(shè)為默認(rèn)值false;

    • 使用瀏覽器打開tests/unit/coverage/lcov-report路徑下的index.html文件即可瀏覽各個被測試的文件的詳細(xì)覆蓋信息。
      vue框架自動化測試,jest,vue,JavaScript,前端,vue.js,單元測試
  • collectCoverageFrom:設(shè)置收集覆蓋率的文件范圍;

    • 通常業(yè)務(wù)代碼編寫在src/views中,因此此處設(shè)置src/views下的js,vue文件;
    • 同時src/components中部分組件不希望在覆蓋率中被捕捉,因此可單獨(dú)配置希望進(jìn)行收集的目錄;
    • 可以通過在前方配置!設(shè)置某目錄下不進(jìn)行覆蓋率收集,例如上方node_modules。
  • coverageDirectory:覆蓋率報(bào)告生成位置,運(yùn)行npm run test:unit命令跑單測即可生成,配合.gitignore不將覆蓋率報(bào)告提交至git倉庫;
    vue框架自動化測試,jest,vue,JavaScript,前端,vue.js,單元測試

  • coverageThreshold:支持設(shè)置statements、branches、functions、lines四種指標(biāo)的最低覆蓋率,當(dāng)未符合設(shè)置閾值時,則判定單元測試失敗,后續(xù)通過設(shè)置不同業(yè)務(wù)的覆蓋率閾值來完成與Jenkins的對接;

    • 支持為某個路徑下的文件單獨(dú)進(jìn)行閾值設(shè)置
    • 當(dāng)設(shè)置負(fù)數(shù)-n時,則為未覆蓋率不允許超過n%。
  • setupFiles:在運(yùn)行單元測試前,先運(yùn)行的文件,用于進(jìn)行預(yù)制配置的設(shè)置,例如接口mock、插件配置、封裝方法等;

3、 目錄結(jié)構(gòu)

實(shí)際開發(fā)過程中,我們應(yīng)當(dāng)具備較為完善的自動化測試目錄結(jié)構(gòu):
vue框架自動化測試,jest,vue,JavaScript,前端,vue.js,單元測試

(1) .eslintrc.js

module.exports = {
  env: {
    jest: true,
  },
  globals: {
    utils: "writalbe",
    $: "writalbe",
    moment: "writalbe",
  },
};

配置在unit目錄下的eslint規(guī)則。

  • 聲明環(huán)境為jest以此保證使用jest api時不會觸發(fā)Eslint報(bào)錯;
  • 由于上方將utils注冊到global中,后續(xù)使用直接通過utils.[functionName]調(diào)用,此處將utils設(shè)置為全局變量,實(shí)現(xiàn)在測試腳本中直接使用utils不會出現(xiàn)Eslint報(bào)錯,$、moment同理。

(2) setup

main.setup.js

import "./api"; // api Mock
import './utils' // 工具方法
import './plugins' // 插件聲明

按照順序進(jìn)行引入,優(yōu)先聲明方法mock/插件聲明,后引入預(yù)置配置和工具方法。

plugins目錄

// index.js
import "./global";

插件聲明入口文件,統(tǒng)一引入,下方舉例。

// global.js
import Vue from 'vue'
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
import { parseTime, resetForm } from "@/utils/general";
import { hasPermi } from "@/directives/hasPermi";
import Pagination from "@/components/Pagination";
import ebDialog from "@/components/eb-components/EB-dialog";

Vue.prototype.msgSuccess = function (msg) {
  this.$message({ showClose: true, message: msg, type: "success" });
};

Vue.prototype.msgWarning = function (msg) {
  this.$message({ showClose: true, message: msg, type: "warning" });
};

Vue.prototype.msgError = function (msg) {
  this.$message({ showClose: true, message: msg, type: "error" });
};

Vue.use(ElementUI);
Vue.prototype.parseTime = parseTime;
Vue.prototype.resetForm = resetForm;
Vue.directive("hasPermi", { hasPermi });
Vue.component("Pagination", Pagination);
Vue.component("ebDialog", ebDialog);

通過上述方式,將所需插件進(jìn)行注冊:

  • jest在執(zhí)行測試腳本時,不會像正常執(zhí)行過程中優(yōu)先執(zhí)行main.js,例如在測試腳本中渲染materialList/index.vue,此時只會執(zhí)行該文件的生命周期,因此需要通過該種方式對公用插件進(jìn)行全局注冊,保證測試腳本的正常執(zhí)行;
  • 同樣,后續(xù)在引入其余插件時,應(yīng)在該文件同級目錄下創(chuàng)建相應(yīng)以插件名稱命名的文件,并在index.js中引入。

utils目錄

// index.js
import { timeout, request, response, mockApi } from "./api"; // api 封裝方法
import { // 工具類封裝方法
  getTablesHeader, // 獲取表頭
  getTablesData, // 獲取表格數(shù)據(jù)
  getTablesAction, // 獲取表格操作列
  getButton, // 獲取按鈕
  getTableButton, // 獲取表格按鈕
  getModalTitles, // 獲取彈窗標(biāo)題
  getModalCloses, // 獲取彈窗關(guān)閉按鈕
  getNotificationsContent, // 獲取Notification提示
  removeNotifications, // 移除Notification提示
  getConfirmsContent, // 獲取Confirm氣泡確認(rèn)框內(nèi)容
  getConfirmButton, // 獲取Confirm氣泡確認(rèn)框按鈕
  getMessageContent, // 獲取Message信息內(nèi)容
  getFormItems, // 獲取表單項(xiàng)
  getFormErrors, // 獲取表單校驗(yàn)失敗信息
  getSelect, // 獲取下拉框
  // 以下未實(shí)現(xiàn),需要使用請自行封裝
  getActiveTabs,
  getTabButton,
  getCheckboxs,
  getIcon,
  getTableSelections,
  getBreadcrumbButton,
  getDropdownOptions,
  getDropdownButton,
  getSelectOption,
  getAllowClear,
  getModalClose,
} from "./element-ui";

global.utils = {
  // api
  timeout,
  request,
  response,
  mockApi,
  // element-ui
  getTablesHeader,
  getTablesData,
  getTablesAction,
  getButton,
  getTableButton,
  getModalTitles,
  getModalCloses,
  getNotificationsContent,
  removeNotifications,
  getConfirmsContent,
  getConfirmButton,
  getMessageContent,
  getFormItems,
  getFormErrors,
  getSelect,
  // 以下未實(shí)現(xiàn),需要使用請自行封裝
  getActiveTabs,
  getTabButton,
  getCheckboxs,
  getIcon,
  getTableSelections,
  getBreadcrumbButton,
  getDropdownOptions,
  getDropdownButton,
  getSelectOption,
  getAllowClear,
  getModalClose,
};

工具方法注冊入口文件,統(tǒng)一引入常用的封裝方法,并將其注冊置global.utils中,在后續(xù)測試腳本中無需import,直接通過utils.${functionName}進(jìn)行調(diào)用。

// api.js
// 延時器
export function timeout (time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve()
    }, time)
  })
}

// 模擬接口請求
export function request () {
  return jest.fn(
    (params) =>
      utils.response({})
  )
}
// 模擬接口響應(yīng)
export function response (data) {
  return new Promise((resolve, reject) => {
    resolve(data)
  })
}
// 自定義mock-修改單一api響應(yīng)數(shù)據(jù)
export function mockApi (mock, api, data) {
  mock[api].mockImplementation(() => utils.response(data))
}

在單元測試中,需要模擬接口響應(yīng)在多種情況下的不同場景,尤其是在真實(shí)開發(fā)環(huán)境中不好模擬的場景,例如存在時間極短的中間狀態(tài)等。

  • 在原先的Jest調(diào)研中,選擇的mock對象為axios方法,而通過mock axios時,無法做到靈活的多組mock數(shù)據(jù)的使用;在本次調(diào)研中選擇mock各個api,并通過mockImplementation實(shí)現(xiàn)在describe以及it中的數(shù)據(jù)更改,由此實(shí)現(xiàn)靈活的多組mock實(shí)現(xiàn),來覆蓋更多場景。

api目錄

// index.js
jest.mock("@/api/materialList/materialList", () =>
  require("@/../tests/unit/setup/api/materialList.mock"),
);
jest.mock("@/api/categoryManage/categoryManage", () =>
  require("@/../tests/unit/setup/api/categoryManage.mock"),
);

通過jest.mock模擬api中的相應(yīng)方法,達(dá)到全局api初始化,與views/api中的文件對應(yīng),在api目錄下創(chuàng)建對應(yīng)文件名的.mock.js文件。

export const getMaterialList = utils.request();
export const getjudgeCategory = utils.request();
export const addMaterial = utils.request();
export const getMaterialDetail = utils.request();
export const updateMaterial = utils.request();

在對應(yīng)文件的.mock.js文件中,通過上述方式聲明業(yè)務(wù)代碼中的各api函數(shù),上述含義為將所聲明接口返回值初始化為空對象{},使用jest.fn進(jìn)行接口模擬,通過utils.response返回promise,模擬接口響應(yīng)。

  • 文件命名與src/api中相應(yīng)文件相同,即如src/api/materialList.js中的api則此處應(yīng)創(chuàng)建materialList.mock.js文件。

(3) specs

specs中的目錄結(jié)構(gòu)應(yīng)與項(xiàng)目所測試目錄保持一致,例如views/materialManage/materialList/index.vue的測試腳本在specs中應(yīng)在views/materialManage/materialList目錄下,以此保持單元測試代碼的可讀/可維護(hù)性,下方以materialList目錄下的index.vue文件舉例(此處僅展示基本流程,具體用例編寫參見后續(xù)樣例)。

// materialList.spec.js
import { mount } from "@vue/test-utils";
import materialList from "@/views/materialManage/materialList/index.vue";
import mockData from "./mockData";

const materialListApi = require("@/../tests/unit/setup/api/materialList.mock");
const categoryManageApi = require("@/../tests/unit/setup/api/categoryManage.mock");
utils.mockApi(
  materialListApi,
  "getMaterialList",
  mockData.success.getMaterialList,
);
describe("素材列表頁", () => {
  const wrapper = mount(materialList);
  const _this = wrapper.vm;
  
  it("素材列表頁-查詢失敗", async () => {
    utils.mockApi(
      materialListApi,
      "getMaterialList",
      mockData.failure.getMaterialList,
    );
    _this.pageList = [];
    _this.total = 0;
    _this.loading = false;
    await utils.getButton(wrapper, "搜索").trigger("click");
    expect(_this.pageList).toEqual([]);
    expect(_this.total).toBe(0);
    expect(_this.loading).toBe(true);
  });
  
  it("素材列表頁-查詢成功", async () => {
    utils.mockApi(
      materialListApi,
      "getMaterialList",
      mockData.success.getMaterialList,
    );
    _this.pageList = [];
    _this.total = 0;
    _this.loading = false;
    await utils.getButton(wrapper, "搜索").trigger("click");
    let expectData = mockData.success.getMaterialList.data;
    expect(_this.pageList).toEqual(expectData.list);
    expect(_this.total).toBe(expectData.total);
    expect(_this.loading).toBe(false);
  });
});

上方示例中通過jest.mock模擬api中的materialList文件的相應(yīng)方法,下方通過utils.mockApi對getMaterialList進(jìn)行重新處理,實(shí)現(xiàn)靈活的mock數(shù)據(jù)修改。

// mockData.js
const mockData = {
  success: {
    getMaterialList: {
      code: 200,
      data: {
        total: 83,
        list: [
          {
            md5File: "969e0a368a3a3ec423fccc39433c7427",
            materialUrl:
              "https://rcs.telinovo.com/material/96/9e0a368a3a3ec423fccc39433c7427.mp4",
            showUrl: null,
            dir: "96",
            realName: "9e0a368a3a3ec423fccc39433c7427.mp4",
            createTime: "2022-12-23T06:19:31.000+0000",
            categoryId: 2,
            materialName: "測試視頻",
            phone: null,
            fileType: 2,
            categoryName: "默認(rèn)分類/默認(rèn)分類",
          },
          {
            md5File: "ae543e4e6d8706faee63ed3be07f1b7c",
            materialUrl:
              "https://rcs.telinovo.com/material/ae/543e4e6d8706faee63ed3be07f1b7c.png",
            showUrl: null,
            dir: "ae",
            realName: "543e4e6d8706faee63ed3be07f1b7c.png",
            createTime: "2022-12-22T08:58:27.000+0000",
            categoryId: 55,
            materialName: "關(guān)注攻略",
            phone: null,
            fileType: 1,
            categoryName: "活動圖片/封面圖片",
          },
        ],
      },
      message: "操作成功",
    },
  },
  failure: {
    getMaterialList: {
      code: 500,
      data: null,
      message: "操作失敗",
    },
  },
};

export default mockData;

在mockData中分別設(shè)置success,failure時的api mock數(shù)據(jù),該種方式利于后續(xù)在斷言中進(jìn)行響應(yīng)結(jié)果判斷。

(4) StyleMock.js

module.exports = {}

上述moduleNameMapper提到Jest運(yùn)行無法識別import .css/.less等后綴,將其映射到該js文件,此處直接exports空對象保證測試腳本正常執(zhí)行。

  • 單元測試本身不關(guān)注樣式,但關(guān)注dom結(jié)構(gòu)

4、 Api

(1) vue-test-utils

vue-test-utils主要負(fù)責(zé)節(jié)點(diǎn)獲取,編寫測試邏輯。下面列舉幾個常用的Api,以及介紹一下wrapper對象。
Api

  • mount
    創(chuàng)建一個包含被掛載和渲染的 Vue 組件的 Wrapper。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

describe('Foo', () => {
  it('renders a div', () => {
    const wrapper = mount(Foo)
    expect(wrapper.contains('div')).toBe(true)
  })
})
  • shallowMount
    和mount一樣,創(chuàng)建一個包含被掛載和渲染的 Vue 組件的 Wrapper,與shallowMount區(qū)別:
    • mount會渲染整個組件樹而shallowMount會對子組件存根;
    • shallowMount可以確保你對一個組件進(jìn)行獨(dú)立測試,有助于避免測試中因子組件的渲染輸出而混亂結(jié)果。
import { shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'

describe('Foo', () => {
  it('renders a div', () => {
    const wrapper = shallowMount(Foo)
    expect(wrapper.contains('div')).toBe(true)
  })
})

Wrapper
Wrapper 是一個對象,該對象包含了一個掛載的組件或 vnode,以及測試該組件或 vnode 的方法。
下面介紹一些它的常用方法。

  • attributes
    返回 Wrapper DOM 節(jié)點(diǎn)的特性對象。如果提供了 key,則返回這個 key 對應(yīng)的值。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

const wrapper = mount(Foo)
expect(wrapper.attributes().id).toBe('foo')
expect(wrapper.attributes('id')).toBe('foo')
  • classes
    返回 Wrapper DOM 節(jié)點(diǎn)的 class。
    返回 class 名稱的數(shù)組?;蛟谔峁?class 名的時候返回一個布爾值。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

const wrapper = mount(Foo)
expect(wrapper.classes()).toContain('bar')
expect(wrapper.classes('bar')).toBe(true)
  • contains
    判斷 Wrapper 是否包含了一個匹配選擇器的元素或組件。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

const wrapper = mount(Foo)
expect(wrapper.contains('p')).toBe(true)
expect(wrapper.contains(Bar)).toBe(true)
  • find
    返回匹配選擇器的第一個 DOM 節(jié)點(diǎn)或 Vue 組件的 Wrapper。
    可以使用任何有效的 DOM 選擇器 (使用 querySelector 語法)。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

const wrapper = mount(Foo)

const div = wrapper.find('div')
expect(div.exists()).toBe(true)

const byId = wrapper.find('#bar')
expect(byId.element.id).toBe('bar')
  • findAll
    返回一個 WrapperArray。
    可以使用任何有效的選擇器。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

const wrapper = mount(Foo)

const div = wrapper.findAll('div').at(0)
expect(div.is('div')).toBe(true)

const bar = wrapper.findAll(Bar).at(0) // 已廢棄的用法
expect(bar.is(Bar)).toBe(true)
  • findComponent
    返回第一個匹配的 Vue 組件的 Wrapper。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

const wrapper = mount(Foo)

const bar = wrapper.findComponent(Bar) // => 通過組件實(shí)例找到 Bar
expect(bar.exists()).toBe(true)
const barByName = wrapper.findComponent({ name: 'bar' }) // => 通過 `name` 找到 Bar
expect(barByName.exists()).toBe(true)
const barRef = wrapper.findComponent({ ref: 'bar' }) // => 通過 `ref` 找到 Bar
expect(barRef.exists()).toBe(true)
  • findAllComponents
    為所有匹配的 Vue 組件返回一個 WrapperArray。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

const wrapper = mount(Foo)
const bar = wrapper.findAllComponents(Bar).at(0)
expect(bar.exists()).toBeTruthy()
const bars = wrapper.findAllComponents(Bar)
expect(bars).toHaveLength(1)
  • html
    返回 Wrapper DOM 節(jié)點(diǎn)的 HTML 字符串。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

const wrapper = mount(Foo)
expect(wrapper.html()).toBe('<div><p>Foo</p></div>')
  • text
    返回 Wrapper 的文本內(nèi)容。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

const wrapper = mount(Foo)
expect(wrapper.text()).toBe('bar')
  • is
    斷言 Wrapper DOM 節(jié)點(diǎn)或 vm 匹配選擇器。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

const wrapper = mount(Foo)
expect(wrapper.is('div')).toBe(true)
  • setData
    設(shè)置 Wrapper vm 的屬性。
    setData 通過遞歸調(diào)用 Vue.set 生效。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

test('setData demo', async () => {
  const wrapper = mount(Foo)
  await wrapper.setData({ foo: 'bar' })
  expect(wrapper.vm.foo).toBe('bar')
})
  • trigger
    在該 Wrapper DOM 節(jié)點(diǎn)上異步觸發(fā)一個事件。
import { mount } from '@vue/test-utils'
import Foo from './Foo'

test('trigger demo', async () => {
  const wrapper = mount(Foo)

  await wrapper.trigger('click')

  await wrapper.trigger('click', {
    button: 0
  })

  await wrapper.trigger('click', {
    ctrlKey: true // 用于測試 @click.ctrl 處理函數(shù)
  })
})

WrapperArray
一個 WrapperArray 是一個包含 Wrapper 數(shù)組以及 Wrapper 的測試方法等對象。
下面介紹一些它的常用方法。

  • at
    返回第 index 個傳入的 Wrapper 。數(shù)字從 0 開始計(jì)數(shù) (比如第一個項(xiàng)目的索引值是 0)。如果 index 是負(fù)數(shù),則從最后一個元素往回計(jì)數(shù) (比如最后一個項(xiàng)目的索引值是 -1)。
import { shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'

const wrapper = shallowMount(Foo)
const divArray = wrapper.findAll('div')

const secondDiv = divArray.at(1)
expect(secondDiv.is('div')).toBe(true)

const lastDiv = divArray.at(-1)
expect(lastDiv.is('div')).toBe(true)
  • filter
    用一個針對 Wrapper 的斷言函數(shù)過濾 WrapperArray。
    該方法的行為和 Array.prototype.filter 相同。
import { shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'

const wrapper = shallowMount(Foo)
const filteredDivArray = wrapper
  .findAll('div')
  .filter(w => !w.hasClass('filtered'))
  • setData
    為 WrapperArray 的每個 Wrapper vm 都設(shè)置數(shù)據(jù)。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

test('setData demo', async () => {
  const wrapper = mount(Foo)
  const barArray = wrapper.findAll(Bar)
  await barArray.setData({ foo: 'bar' })
  expect(barArray.at(0).vm.foo).toBe('bar')
})
  • trigger
    為 WrapperArray 的每個 Wrapper DOM 節(jié)點(diǎn)都觸發(fā)一個事件。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'

test('trigger demo', async () => {
  const wrapper = mount(Foo)

  const divArray = wrapper.findAll('div')
  await divArray.trigger('click')
})

更多信息詳見Vue Test Utils 中文官方文檔。

(2) Jest

Jest主要負(fù)責(zé)對測試結(jié)果進(jìn)行斷言。下面例舉一些常用斷言函數(shù)。

  • except(data).toBe(value):判斷expect內(nèi)容是否與value相同;
  • except(data).toBeTruthy():除了false , 0 , ‘’ , null , undefined , NaN都將通過;
  • except(data).toBeFalsy():與上述相反;
  • except(data).toEqual(value):比較Object/Array是否相同。

更多信息詳見Jest中文文檔。

(3) 封裝工具

以下例舉部分封裝的工具方法。

  • 獲取表格數(shù)據(jù)
/**
 * 獲取全部表格-數(shù)據(jù)
 * @param {wrapper}
 * @param {scrollable}
 * @returns {Object}
 */
export function getTablesData(wrapper) {
  let result = {};
  let tables = wrapper.findAll(".el-table");

  for (let tableIndex = 0; tableIndex < tables.length; tableIndex++) {
    result["table-" + tableIndex] = {};
    let headers;
    headers = tables.at(tableIndex).find(".el-table__header").findAll("th");

    let titles = [];
    let operation = false;
    for (let headerIndex = 0; headerIndex < headers.length; headerIndex++) {
      let title = headers.at(headerIndex).find(".cell").text();
      titles.push(title);
      if (
        headerIndex === headers.length - 1 &&
        headers.at(headerIndex).find(".cell").text().includes("操作")
      ) {
        operation = true;
      }
    }

    let rows = tables
      .at(tableIndex)
      .find(".el-table__body")
      .findAll(".el-table__row");
    for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
      result["table-" + tableIndex]["row-" + rowIndex] = {};
      let tds = rows.at(rowIndex).findAll("td");
      for (let tdIndex = 0; tdIndex < tds.length; tdIndex++) {
        if (tdIndex < tds.length - 1 || !operation) {
          let td = tds.at(tdIndex);
          // 由于圖片使用的el-image,它會異步渲染真實(shí)圖片,優(yōu)先渲染el-image__placeholder,所以同步代碼中是拿不到真實(shí)圖片的
          // 解決方案:使用el-image時,添加placeholder插槽,自定義傳入圖片資源地址

          if (td.findAll("img").length) {
            result["table-" + tableIndex]["row-" + rowIndex][titles[tdIndex]] =
              td.find("img").attributes("src");
          }
          if (td.findAll("video").length) {
            result["table-" + tableIndex]["row-" + rowIndex][titles[tdIndex]] =
              td.find("video").attributes("src");
          }
          if (!td.findAll("img").length && !td.findAll("video").length) {
            result["table-" + tableIndex]["row-" + rowIndex][titles[tdIndex]] =
              td.text();
          }
        }
      }
    }
  }

  return result;
}
  • 獲取表單項(xiàng)
/**
 * 獲取全部表單項(xiàng)信息
 * @param {wrapper}
 * @returns {Array}
 */
export async function getFormItems(wrapper) {
  await wrapper.vm.$nextTick();
  let res = [];

  // 后面的元素會覆蓋前面的
  let types = [
    "el-radio",
    "el-radio-group",
    "el-checkbox",
    "el-checkbox-group",
    "el-input",
    "el-input-number",
    "el-select",
    "el-cascader",
    "el-switch",
    "el-slider",
    "el-date-editor--time-select",
    "el-date-editor--time",
    "el-date-editor--timerange",
    "el-date-editor--date",
    "el-date-editor--dates",
    "el-date-editor--week",
    "el-date-editor--month",
    "el-date-editor--months",
    "el-date-editor--year",
    "el-date-editor--years",
    "el-date-editor--daterange",
    "el-date-editor--monthrange",
    "el-date-editor--datetime",
    "el-date-editor--datetimerange",
    "el-upload",
    "el-rate",
    "el-color-picker",
    "el-transfer",
  ];
  let formItems = $(
    $("body").find(".el-form")[$("body").find(".el-form").length - 1],
  ).find(".el-form-item");
  if (!formItems.length) {
    formItems = $(
      $(wrapper.html()).find(".el-form")[
        $(wrapper.html()).find(".el-form").length - 1
      ],
    ).find(".el-form-item");
  }
  Array.from(formItems).forEach(formItem => {
    let required = false;
    let classArr = $(formItem).attr("class").split(" ");

    if (classArr.filter(item => item.includes("required")).length) {
      required = true;
    }

    let label = $(formItem).find(".el-form-item__label").text();

    let disabled = $(formItem).html().includes("disabled");

    let type = "";
    let htmlContent = $(formItem).find(".el-form-item__content").html();
    types.forEach(item => {
      if (htmlContent.includes(item)) {
        if (item === "el-date-editor--time-select") {
          type = "el-time-select";
        } else if (
          item === "el-date-editor--time" ||
          item === "el-date-editor--timerange"
        ) {
          type = "el-time-picker";
        } else if (
          item === "el-date-editor--date" ||
          item === "el-date-editor--dates" ||
          item === "el-date-editor--week" ||
          item === "el-date-editor--month" ||
          item === "el-date-editor--months" ||
          item === "el-date-editor--year" ||
          item === "el-date-editor--years" ||
          item === "el-date-editor--daterange" ||
          item === "el-date-editor--monthrange" ||
          item === "el-date-editor--datetime" ||
          item === "el-date-editor--datetimerange"
        ) {
          type = "el-date-picker";
        } else {
          type = item;
        }
      }
    });

    res.push({
      label: label,
      required: required,
      type: type,
      disabled: disabled,
    });
  });

  return res;
}
  • 獲取表單校驗(yàn)失敗信息
/**
 * 獲取全部表單報(bào)錯信息
 * @param {wrapper}
 * @returns {Array}
 */
export async function getFormErrors(wrapper) {
  await wrapper.vm.$nextTick();

  let result = [];

  let formItems = $(
    $("body").find(".el-form")[$("body").find(".el-form").length - 1],
  ).find(".el-form-item");
  if (!formItems.length) {
    formItems = $(
      $(wrapper.html()).find(".el-form")[
        $(wrapper.html()).find(".el-form").length - 1
      ],
    ).find(".el-form-item");
  }
  Array.from(formItems).forEach(formItem => {
    let field = $(formItem).find(".el-form-item__label").attr("for");
    let label = $(formItem).find(".el-form-item__label").text();
    let error = $(formItem).find(".el-form-item__error").text().trim();

    result.push({
      field,
      label,
      error,
    });
  });

  return result;
}

4、 UI測試

TODO

5、 E2E測試

TODO

到了這里,關(guān)于前端自動化測試(二)Vue Test Utils + Jest的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 前端單元測試與自動化測試實(shí)踐

    在前端開發(fā)中,單元測試和自動化測試是保證代碼質(zhì)量和穩(wěn)定性的重要手段。通過編寫和執(zhí)行測試用例,可以及早發(fā)現(xiàn)代碼中的問題,并確保代碼在不同環(huán)境下的正確運(yùn)行。本文將介紹前端單元測試和自動化測試的實(shí)踐,并通過一個示例說明其重要性和具體操作。 前端單元測

    2024年02月12日
    瀏覽(24)
  • 前端自動化測試 —— Jest 測試框架應(yīng)用

    前端自動化測試 —— Jest 測試框架應(yīng)用

    目錄 ??????? 什么是自動化測試 為什么要用前端自動化測試 前端自動化分類和思想 單元測試 集成測試 TDD 測試驅(qū)動開發(fā)(Test Driven Development) BDD 行為驅(qū)動開發(fā)(Behavior Driven Development) 如何自己寫非框架測試用例 是否能簡化? 如何能清晰地看到我測的是哪個呢?

    2024年02月07日
    瀏覽(18)
  • 前端自動化測試框架-Cypress

    前端自動化測試框架-Cypress

    一提起 Web UI 自動化時,大多數(shù)都會想到自動化測試工具 Selenium。隨著測試技術(shù)的不斷發(fā)展,出現(xiàn)了很多優(yōu)秀的自動化測試工具。 本篇將介紹一款目前市面上很受歡迎的自動化測試工具-Cypress。 Cypress 是一個易于使用、快速穩(wěn)定、可靠性高、全面性強(qiáng)的自動化測試框架,因此

    2024年02月10日
    瀏覽(18)
  • 前端自動化測試之葵花寶典

    作者:京東零售 杜興文 首先聊一下概念,Web 前端自動化測試是一種通過編寫代碼來自動化執(zhí)行 Web 應(yīng)用程序的測試任務(wù)的方法,它通常使用 JavaScript 和測試框架 (如 Selenium、Appium 等) 來實(shí)現(xiàn)。 Web 前端自動化測試的優(yōu)點(diǎn)是可以提高測試效率、減少測試時間和測試成本,并且可

    2023年04月13日
    瀏覽(17)
  • vitest單元測試配合@vue/test-utils之組件單元測試篇

    vitest 是由 vite 提供支持的極速單元測試框架,VueTestUtils 是 Vue.js 的官方測試實(shí)用程序庫,vitest 本身是不支持單元組件測試的,需要配合 test-utils 來完成組件單元測試,安裝與基本 API 就不再贅述,學(xué)會閱讀文檔與查找資料是一個程序員的基本功 demo 由一個組件和測試文件組成

    2024年03月16日
    瀏覽(26)
  • 一文,教你搭建前端自動化測試環(huán)境

    一文,教你搭建前端自動化測試環(huán)境

    前言 最近在看前端自動化測試相關(guān)的東西,在搭建環(huán)境的時候發(fā)現(xiàn)還是有許多需要注意的地方,而且網(wǎng)上很少有將各種測試(單元測試,集成測試,端對端測試)的環(huán)境搭建都提及的文章,對像我這樣的新手不太友好,于是便打算刪繁就簡,希望通過這一篇文章能讓大家 對

    2024年02月16日
    瀏覽(24)
  • 漫談前端自動化測試演進(jìn)之路及測試工具分析

    作者:京東零售 杜興文 隨著前端技術(shù)的不斷發(fā)展和應(yīng)用程序的日益復(fù)雜,前端自動化測試也在不斷演進(jìn)。 Web 前端 UI 自動化測試發(fā)展史可以追溯到 2000 年,當(dāng)時最早的 Web 應(yīng)用程序越來越復(fù)雜,開發(fā)人員開始使用自動化測試工具來確保應(yīng)用程序的正確性和可靠性。 在早期,

    2023年04月19日
    瀏覽(28)
  • vitest 單元測試配合@vue/test-utils 之 axios 篇

    vitest 是由 vite 提供支持的極速單元測試框架,VueTestUtils 是 Vue.js 的官方測試實(shí)用程序庫,Axios 是一個基于 promise 的網(wǎng)絡(luò)請求庫,以上均為各自官網(wǎng)對其的描述 項(xiàng)目中使用 axios 是非常常見的,所以我們可以對他做一個單元測試,在 test-utils 的文檔中提到除了 jest.mock()還可以使

    2024年02月19日
    瀏覽(23)
  • 前端自動化測試工具 Cypress 試用調(diào)研記錄

    目錄 前言 環(huán)境準(zhǔn)備 1.工具:vs code;環(huán)境:node.js。 2.安裝 cypress 3.安裝插件: 4.配置: 5.啟動命令: helloworld: 第一個用例 元素定位方式 使用 request 請求進(jìn)行登錄 提取登錄方法為公共方法 命令行執(zhí)行所有用例 解決 chrome 下的跨域問題: 生成 Junit-allure 報(bào)表 生成 mocha awsome

    2024年02月16日
    瀏覽(24)
  • 記錄使用vue-test-utils + jest 在uniapp中進(jìn)行單元測試

    uniapp推薦了測試方案 @dcloudio/uni-automator ,屬于自動化測試,api提供的示例偏重于渲染組件,判斷當(dāng)前渲染的組件是否和預(yù)期一致 vue推薦的測試方案 vue test utils ,屬于單元測試,可以搭配jest、mocha等單測運(yùn)行器 我選了方案2??? 關(guān)于vue的組件測試,vue官方提到: 你的 Vue 應(yīng)用

    2024年02月06日
    瀏覽(31)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包