試指南:基于 Cypress 的端到端測(cè)試實(shí)踐)
后端前端即時(shí)通訊社交【免費(fèi)下載鏈接】spectrumSimple, powerful online communities.項(xiàng)目地址https://gitcode.com/gh_mirrors/sp/spectrum點(diǎn)擊查看免費(fèi)下載Spectrum 的集成測(cè)試Integration Tests用于在真實(shí)瀏覽器環(huán)境中驗(yàn)證「API 前端客戶端」協(xié)同工作的完整鏈路確保社區(qū)、頻道、帖子、消息等核心功能持續(xù)可用。本文基于 docs/testing/integration.md 展開結(jié)合 package.json、cypress.json 以及 cypress/ 目錄下的真實(shí)測(cè)試代碼完整講解環(huán)境搭建、測(cè)試編寫、data-cy選擇器約定與測(cè)試數(shù)據(jù)庫機(jī)制幫助你直接上手為 Spectrum 編寫可運(yùn)行的端到端測(cè)試。集成測(cè)試在 Spectrum 測(cè)試體系中的定位Spectrum 的測(cè)試體系由兩部分構(gòu)成參見 docs/testing/intro.md單元測(cè)試Unit tests主要集中在api/下使用 Jest 驗(yàn)證 API 層的模型、工具函數(shù)與解析器邏輯集成測(cè)試Integration tests使用 Cypress 驅(qū)動(dòng)真實(shí)瀏覽器從頁面層驗(yàn)證「客戶端渲染 GraphQL API 數(shù)據(jù)庫」整條鏈路是否按預(yù)期工作。整個(gè)測(cè)試套件會(huì)在每次提交commit和每個(gè) PR 的 CI 中運(yùn)行一旦有破壞性改動(dòng)導(dǎo)致 CI 失敗該 PR 將無法合并。集成測(cè)試正是守住前端交互與 API 契約的最后一道防線。從 package.json 的腳本可以看出倉庫為集成測(cè)試準(zhǔn)備了完整的命令鏈build:api、start:api:test、dev:web、cypress:open以及用于 CI 的無頭模式腳本test:e2e。這些腳本的用途會(huì)在下文逐一展開。前置環(huán)境以生產(chǎn)模式 API 測(cè)試數(shù)據(jù)庫 前端客戶端搭建運(yùn)行環(huán)境與單元測(cè)試不同集成測(cè)試需要整套服務(wù)真實(shí)運(yùn)行。文檔明確說明API 必須以生產(chǎn)模式production mode構(gòu)建運(yùn)行并連接到測(cè)試數(shù)據(jù)庫同時(shí)前端客戶端web client也必須在本地啟動(dòng)。之所以要求生產(chǎn)模式是因?yàn)樯a(chǎn)構(gòu)建build-api更接近線上行為避免開發(fā)模式下熱更新、調(diào)試日志等帶來的不確定性。第一步構(gòu)建 APIyarn run build:api該命令實(shí)際執(zhí)行的是 package.json 中的prebuild:api: rimraf build-api, build:api: cross-env NODE_PATH./ cross-env DIRapi backpack build, postbuild:api: cp api/package.json build-api/package.json cp now.json build-api/now.json cpy cacert build-api即先清空舊的build-api/目錄再用 backpack 以DIRapi為入口構(gòu)建 API構(gòu)建完成后把a(bǔ)pi/package.json、now.json與cacert一并拷貝進(jìn)產(chǎn)物目錄保證build-api/main.js可獨(dú)立運(yùn)行。第二步在第一個(gè)終端啟動(dòng)測(cè)試模式的 APIyarn run start:api:test對(duì)應(yīng)腳本為 package.jsonprestart:api:test: node -e \require(./shared/testing/setup.js)().then(() process.exit())\, start:api:test: TEST_DBtrue FORCE_DEVtrue DEBUGapi*,shared* forever build-api/main.js這里有兩個(gè)值得注意的細(xì)節(jié)prestart:api:test鉤子在 API 啟動(dòng)前自動(dòng)執(zhí)行 shared/testing/setup.js完成測(cè)試數(shù)據(jù)庫的遷移與種子數(shù)據(jù)寫入詳見下文「測(cè)試數(shù)據(jù)庫機(jī)制」一節(jié)因此你無需手動(dòng)初始化數(shù)據(jù)庫環(huán)境變量組合TEST_DBtrue讓 API 連接測(cè)試數(shù)據(jù)庫庫名為testingFORCE_DEVtrue強(qiáng)制以開發(fā)輔助模式運(yùn)行用于測(cè)試場(chǎng)景同時(shí)通過DEBUGapi*,shared*打開 API 與 shared 層的調(diào)試日志方便排查問題。進(jìn)程由forever托管保證 API 在集成測(cè)試期間持續(xù)存活。第三步在另一個(gè)終端啟動(dòng) Web 客戶端yarn run dev:web對(duì)應(yīng)腳本為 package.jsondev:web: cross-env NODE_PATH./ react-app-rewired start即通過react-app-rewired啟動(dòng)基于 config-overrides.js 定制的 React 開發(fā)服務(wù)器。Cypress 配置的baseUrl是http://localhost:3000見 cypress.json因此該客戶端默認(rèn)端口為 3000與集成測(cè)試的訪問地址保持一致。第四步打開 Cypress 圖形界面yarn run cypress:open對(duì)應(yīng)腳本 package.json 為cypress open。它會(huì)彈出 Cypress 的 GUI 運(yùn)行器runner你可以看到所有位于cypress/integration/下的測(cè)試文件并逐個(gè)點(diǎn)擊運(yùn)行。對(duì)于需要調(diào)試斷言、觀察頁面狀態(tài)變化的場(chǎng)景GUI 模式下可以實(shí)時(shí)看到每一步命令在瀏覽器中的執(zhí)行過程這也是文檔強(qiáng)調(diào) Cypress「給你一個(gè)很棒的 GUI」的原因。無頭模式面向 CI 的運(yùn)行方式如果不需要 GUI例如在 CI 中倉庫還提供了無頭運(yùn)行腳本test:e2e: CYPRESS_RETRIES2 cypress runCYPRESS_RETRIES2配合依賴 cypress-plugin-retries在 cypress/support/index.js 中通過require(cypress-plugin-retries)加載允許失敗的測(cè)試自動(dòng)重試至多 2 次有效緩解端到端測(cè)試中常見的偶發(fā)性失敗如網(wǎng)絡(luò)抖動(dòng)、動(dòng)畫時(shí)序。cypress run會(huì)以無頭模式依次執(zhí)行全部測(cè)試文件并輸出匯總結(jié)果。測(cè)試數(shù)據(jù)庫機(jī)制遷移 種子數(shù)據(jù) 自動(dòng)復(fù)位集成測(cè)試依賴一個(gè)獨(dú)立且可預(yù)測(cè)的測(cè)試數(shù)據(jù)庫庫名testing相關(guān)實(shí)現(xiàn)集中在 shared/testing/ 目錄。shared/testing/setup.js 在每次運(yùn)行前負(fù)責(zé)兩件事執(zhí)行全部數(shù)據(jù)庫遷移通過rethinkdb-migrate將 api/migrations/ 下的所有遷移腳本按順序應(yīng)用到testing庫寫入種子數(shù)據(jù)從 shared/testing/data.js 讀取預(yù)置數(shù)據(jù)用戶、社區(qū)、頻道、帖子、消息、成員關(guān)系等按表插入并使用{ conflict: replace }保證數(shù)據(jù)可重復(fù)覆蓋。而 shared/testing/teardown.js 負(fù)責(zé)清空testing庫中各表的數(shù)據(jù)。值得一提的是文件頭部的注釋說明之所以不直接用dbDrop(testing)刪庫是因?yàn)閯h庫會(huì)導(dǎo)致 RethinkDB 的 changefeed 斷連、正在運(yùn)行的 API 崩潰因此只清數(shù)據(jù)、保留庫結(jié)構(gòu)。在測(cè)試執(zhí)行層面cypress/support/index.js 在每個(gè)測(cè)試文件的before以及每個(gè)用例的beforeEach中都調(diào)用cy.resetdb()同時(shí)清空 localStorage 與 cookies確保每個(gè)用例都從干凈、確定的數(shù)據(jù)庫狀態(tài)開始杜絕用例之間的數(shù)據(jù)串?dāng)_。cy.resetdb()在 cypress/support/commands.js 中定義本質(zhì)是依次執(zhí)行 teardown 與 setup 兩個(gè) Node 腳本。編寫集成測(cè)試目錄結(jié)構(gòu)、異步隊(duì)列與斷言寫法測(cè)試文件放哪里所有集成測(cè)試文件都位于cypress/integration/目錄下。從倉庫現(xiàn)狀看測(cè)試已按業(yè)務(wù)模塊組織了子目錄cypress/integration/channel/頻道設(shè)置刪除、編輯與頻道視圖成員、資料、帖子列表cypress/integration/community/社區(qū)視圖資料頁cypress/integration/thread/帖子操作欄與帖子視圖cypress/integration/user/用戶刪除、資料編輯、登錄重定向以及頂層的一批功能測(cè)試login_spec.js、thread_spec.js、messages_spec.js、explore_spec.js、toasts_spec.js、modal_routes_spec.js、community_settings_members_spec.js、community_settings_overview_spec.js、user_spec.js。一個(gè)最基本的測(cè)試文檔給出的最簡示例首頁渲染測(cè)試// cypress/integration/home_spec.js describe(Home View, () { before(() { cy.visit(/); }); it(should render the home page, () { cy.get([data-cyhome-page]).should(be.visible); cy.get([href*/login]).should(be.visible); }); });倉庫中與之對(duì)應(yīng)的真實(shí)案例是 cypress/integration/login_spec.jsdescribe(Log in, () { beforeEach(() { cy.visit(/login); }); it(should render login methods, () { cy.get([data-cylogin-page]).should(be.visible); cy.get([href*/auth/twitter]).should(be.visible); cy.get([href*/auth/facebook]).should(be.visible); cy.get([href*/auth/google]).should(be.visible); cy.get([href*/auth/github]).should(be.visible); }); });理解 Cypress 的異步隊(duì)列模型文檔特別提醒Cypress 的 API 表面看起來是同步的底層其實(shí)是完全異步的。每個(gè)cy.*命令如cy.visit、cy.get、cy.contains只是把一條命令追加進(jìn)內(nèi)部隊(duì)列Cypress 會(huì)按順序依次執(zhí)行并自動(dòng)等待上一條命令的結(jié)果滿足條件后才繼續(xù)下一條。因此你不能像寫普通同步 JS 那樣在命令之間直接使用返回值除非通過.then()也不能在cy.visit之后立即用同步代碼斷言頁面狀態(tài)——一切都交給隊(duì)列機(jī)制去編排等待。剛開始可能會(huì)覺得有點(diǎn)繞但熟悉之后會(huì)發(fā)現(xiàn)它天然解決了「等待元素出現(xiàn)」「等待請(qǐng)求完成」這類時(shí)序問題。Cypress 還內(nèi)置了默認(rèn)超時(shí)倉庫在 cypress.json 中把defaultCommandTimeout設(shè)為2000020 秒意味著單個(gè)命令默認(rèn)最多等待 20 秒元素一直不滿足條件才會(huì)失敗比 Cypress 默認(rèn)的 4 秒更寬容適合數(shù)據(jù)量較大、渲染較慢的頁面。Mocha 語法與 Jest 斷言風(fēng)格的差異Cypress 底層使用 Mocha 作為測(cè)試框架而 Spectrum 的單元測(cè)試使用 Jest。這兩者的expect實(shí)現(xiàn)不同語法也有差異單元測(cè)試Jestexpect(value).toEqual(expected)集成測(cè)試Mocha/Chaiexpect(value).to.equal(expected)。從 cypress/integration/thread_spec.js 可以看到真實(shí)的 Chai 斷言用法cy.get([data-cymessage-selected]).should($p { expect($p).to.have.length(1); }); cy.get([data-cymessage]).should($p { expect($p).to.have.length(3); });即to.have.length(n)這類 Chai 鏈?zhǔn)綌嘌浴懽鲿r(shí)需要注意這套語法差異避免把 Jest 的寫法直接搬進(jìn) Cypress 用例。Test IDs用data-cy屬性建立穩(wěn)定的元素定位約定為了保證測(cè)試選擇器不受樣式類名、組件結(jié)構(gòu)重構(gòu)的影響Spectrum 約定在 React 組件上掛載自定義的data-cy屬性。文檔給出的示例class HomePage extends Component { render() { return ( Wrapper>// cypress/integration/home_spec.js it(should render, () { cy.get([data-cyhome-page]).should(be.visible); });這一約定在源碼中廣泛落地例如src/views/login/index.js 掛載data-cylogin-pagesrc/views/thread/container/index.js 掛載data-cythread-viewsrc/components/messageGroup/thread.js 與 src/components/messageGroup/directMessage.js 掛載data-cymessage-groupsrc/components/message/index.js 掛載data-cydelete-message。這些正是 cypress/integration/thread_spec.js 中斷言所依賴的選擇器。采用data-cy而非 CSS class 或 id意味著即使樣式系統(tǒng)styled-components生成的類名發(fā)生變化只要組件語義不變測(cè)試就不會(huì)被破壞——這也是 Cypress 官方 Best Practices 強(qiáng)烈推薦的做法文檔建議在動(dòng)手編寫測(cè)試前先通讀 Cypress 官方文檔的 Best Practices 章節(jié)。真實(shí)用例拆解以thread_spec.js為例的進(jìn)階模式cypress/integration/thread_spec.js 是一個(gè)信息量很大的真實(shí)測(cè)試文件覆蓋了多個(gè)進(jìn)階模式1. 從測(cè)試種子數(shù)據(jù)中取用業(yè)務(wù)對(duì)象測(cè)試直接導(dǎo)入shared/testing/data與api/migrations/seed/default/constants從種子數(shù)據(jù)中按 id 關(guān)聯(lián)出帖子、社區(qū)、作者、版主、消息列表再拼裝出訪問 URL 與預(yù)期斷言import { toPlainText, toState } from ../../shared/draft-utils; import data from ../../shared/testing/data; import { SPECTRUM_PRIVATE_CHANNEL_ID, QUIET_USER_ID, } from ../../api/migrations/seed/default/constants; const thread data.threads[0]; const community data.communities.find( community community.id thread.communityId );2. 權(quán)限場(chǎng)景的差異化斷言同一頁面在「匿名用戶」「帖子作者」「社區(qū)版主」「無權(quán)用戶」四種身份下行為不同。例如帖子作者只能刪除自己的消息delete-message元素僅 1 個(gè)社區(qū)版主可以刪除全部消息delete-message元素有 4 個(gè)無權(quán)用戶訪問私密帖子時(shí)看到的是data-cynull-thread-view。3. 消息內(nèi)容斷言通過shared/draft-utils的toPlainText/toState把 DraftJS 的 JSON 內(nèi)容解析為純文本再與頁面文本比對(duì)cy.contains(toPlainText(toState(JSON.parse(thread.content.body))).split( )[0]);4. 帶查詢參數(shù)的路由測(cè)試訪問/thread/thread-1?mMTQ4MzIyNTIwMDAwMQ驗(yàn)證「定位到某條消息」的深鏈接行為斷言選中的消息數(shù)量與普通消息數(shù)量。自定義命令認(rèn)證、數(shù)據(jù)庫復(fù)位與 DraftJS 輸入cypress/support/commands.js 定義了三個(gè)支撐整套測(cè)試的關(guān)鍵能力cy.auth(userId)注入會(huì)話 Cookie 模擬登錄Cypress.Commands.add(auth, userId { const user getUser(userId); return cy.setCookie( session, encode(JSON.stringify({ passport: { user: user.id } })), { httpOnly: true, secure: false } ); });它從種子數(shù)據(jù)中取出指定用戶把{ passport: { user: userId } }用 api/utils/base64.js 的encode編碼后寫入sessionCookie。這樣測(cè)試無需走完整的 OAuth 登錄流程即可模擬任意用戶普通用戶、帖子作者、社區(qū)版主的登錄態(tài)。用法示例見 cypress/integration/thread_spec.jscy.auth(author.id).then(() cy.visit(/thread/${thread.id}));cy.resetdb()重置數(shù)據(jù)庫到種子狀態(tài)Cypress.Commands.add(resetdb, () { cy.exec(node -e const teardown require(./shared/testing/teardown.js)().then(() process.exit())); cy.exec(node -e const setup require(./shared/testing/setup.js)().then(() process.exit())); });在 cypress/support/index.js 中該命令會(huì)在每個(gè)測(cè)試文件開始前與每個(gè)用例前被調(diào)用配合cy.clearLocalStorage()與cy.clearCookies()保證用例隔離。重寫type支持 DraftJS 富文本編輯器Cypress.Commands.overwrite(type, (originalFn, $elem, text, options) { const textarea $elem[0]; if (textarea.attributes.contenteditable) { var textEvent document.createEvent(TextEvent); textEvent.initTextEvent(textInput, true, true, null, text); textarea.dispatchEvent(textEvent); return Promise.resolve($elem); } else { return originalFn($elem, text, options); } });Spectrum 的消息與帖子編輯使用 DraftJS其編輯器根節(jié)點(diǎn)是contenteditable元素原生cy.type對(duì)其并不友好。這個(gè)覆蓋實(shí)現(xiàn)會(huì)在目標(biāo)是 contenteditable 元素時(shí)派發(fā)TextEvent模擬輸入否則回退到原生type。這是「測(cè)試框架適配業(yè)務(wù)特性」的典型例子。Cypress 配置速覽cypress.json 中與集成測(cè)試直接相關(guān)的配置如下配置項(xiàng)值說明baseUrlhttp://localhost:3000所有cy.visit(/...)的相對(duì)路徑都基于此解析對(duì)應(yīng)dev:web的客戶端端口viewportWidth1300測(cè)試視口寬度保證桌面布局下斷言元素可見defaultCommandTimeout20000單條命令默認(rèn)等待上限 20 秒env.DEBUGsrc*,testing*,build*向測(cè)試注入的調(diào)試命名空間projectId6a92ukCypress Dashboard 項(xiàng)目標(biāo)識(shí)結(jié)合 cypress/plugins/index.jsCypress 插件入口與 cypress/fixtures/example.jsonfixture 示例可以看出整套 Cypress 工程結(jié)構(gòu)是完整且可擴(kuò)展的。常見問題與調(diào)試建議用例之間數(shù)據(jù)互相污染請(qǐng)確認(rèn)每個(gè)用例的beforeEach都執(zhí)行了cy.resetdb()Spectrum 在 cypress/support/index.js 已全局配置新寫的測(cè)試無需重復(fù)添加。元素?cái)嘌圆环€(wěn)定優(yōu)先使用data-cy選擇器保持與源碼約定一致并充分利用cy.contains按可見文本定位對(duì)需要等待的異步渲染20 秒的默認(rèn)超時(shí)一般足夠避免盲目cy.wait(固定毫秒)。偶發(fā)失敗CI 場(chǎng)景下可借助yarn run test:e2e自帶的CYPRESS_RETRIES2重試機(jī)制先排除時(shí)序類抖動(dòng)若重試后仍穩(wěn)定失敗則說明是真實(shí)回歸。區(qū)分?jǐn)嘌哉Z法集成測(cè)試用的是 Chaito.equal、to.have.length不要與 Jest 的toEqual、toHaveLength混用。本地運(yùn)行順序務(wù)必先yarn run build:api再yarn run start:api:test否則 API 產(chǎn)物缺失或版本陳舊會(huì)導(dǎo)致測(cè)試行為與預(yù)期不符start:api:test的prestart鉤子會(huì)自動(dòng)完成測(cè)試庫的遷移與種子數(shù)據(jù)初始化。通過以上流程你就可以在本地完整跑起 Spectrum 的集成測(cè)試并依據(jù)data-cy約定、cy.auth認(rèn)證注入與測(cè)試數(shù)據(jù)庫機(jī)制為新的頁面功能編寫穩(wěn)定、可維護(hù)的端到端用例。相關(guān)源碼均可在 cypress/、shared/testing/ 與 api/migrations/seed/ 中查閱。贊分享后端前端即時(shí)通訊社交【免費(fèi)下載鏈接】spectrumSimple, powerful online communities.項(xiàng)目地址https://gitcode.com/gh_mirrors/sp/spectrum點(diǎn)擊查看免費(fèi)下載相關(guān)推薦spin.js中的端到端測(cè)試Cypress集成實(shí)踐spin.js中的端到端測(cè)試Cypress集成實(shí)踐 為什么需要端到端測(cè)試Spinner組件 在現(xiàn)代Web應(yīng)用中加載狀態(tài)指示器Spinner是提升用戶體驗(yàn)UI組件前端Metabase 端到端測(cè)試實(shí)踐指南基于 Cypress 的 E2E 測(cè)試體系全解析Metabase 端到端測(cè)試實(shí)踐指南基于 Cypress 的 E2E 測(cè)試體系全解析 Metabase 使用 Cypress 構(gòu)建了一套完整的端到端E2E數(shù)據(jù)分析數(shù)據(jù)可視化后端數(shù)據(jù)庫客戶端企業(yè)應(yīng)用runc 集成測(cè)試指南基于 bats 的端到端測(cè)試架構(gòu)與實(shí)踐runc 集成測(cè)試指南基于 bats 的端到端測(cè)試架構(gòu)與實(shí)踐 runc 的集成測(cè)試位于 tests/integration/README.md https:/容器運(yùn)行時(shí)云原生創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考