戰(zhàn)指南:為代碼變更添加單元測試)
開發(fā)工具桌面應(yīng)用【免費(fèi)下載鏈接】desktopFork of GitHub Desktop to support various Linux distributions項(xiàng)目地址https://gitcode.com/gh_mirrors/des/desktop點(diǎn)擊查看免費(fèi)下載本篇技術(shù)指南以 GitHub Desktopdesktop倉庫的 adding-tests.md 為骨架系統(tǒng)講解該項(xiàng)目的測試基礎(chǔ)設(shè)施、單元測試的編寫規(guī)范以及針對(duì)AppStore狀態(tài)更新這類復(fù)雜邏輯的測試策略。讀完本文你將掌握在app/test目錄下創(chuàng)建與組織 Jest 測試模塊、理解yarn test:unit的運(yùn)行機(jī)制并學(xué)會(huì)如何借助純函數(shù)抽取與現(xiàn)有輔助模塊寫出可長期維護(hù)的單元測試。測試基礎(chǔ)設(shè)施總覽倉庫中的所有測試都集中在app/test目錄下并按照測試粒度與用途劃分為若干子目錄。根據(jù) adding-tests.md 的說明其組織方式如下unit—— 針對(duì)代碼庫中較小單元的單元測試目前占測試總量的絕大多數(shù)。其內(nèi)部子目錄的劃分意圖與app/src/的源碼布局保持一致例如unit/git/對(duì)應(yīng)app/src/lib/git/、unit/stores/對(duì)應(yīng)app/src/lib/stores/但該對(duì)應(yīng)關(guān)系并未被嚴(yán)格強(qiáng)制且會(huì)隨著源碼布局的演進(jìn)計(jì)劃原文檔提及的 #5645 重構(gòu)而調(diào)整。integration—— 端到端測試涉及啟動(dòng)應(yīng)用并通過 UI 自動(dòng)化驅(qū)動(dòng)它。此類測試與單元測試的運(yùn)行環(huán)境相互獨(dú)立。除上述兩類測試外還有三個(gè)支撐性目錄fixtures—— 存放可直接用于測試的 Git 倉庫如test-repo、repository-with-105-commits、merge-parser等大量預(yù)置倉庫與文件見 app/test/fixtures。helpers—— 包含用于搭建、管理與銷毀測試環(huán)境的邏輯模塊例如git.ts、temp.ts、random-data.ts、changes-state-helper.ts以及一批repository-builder-*腳手架。__mocks__—— Jest 的特殊目錄用于存放 Electron API 的 mock 實(shí)現(xiàn)僅在被測試代碼需要時(shí)生效當(dāng)前倉庫中僅有一個(gè) electron.ts。此外app/test頂層還有若干基礎(chǔ)設(shè)施文件globals.ts、unit-test-env.ts、setup-test-framework.ts、esm-transformer.js、resolver.js它們共同構(gòu)成了 Jest 的運(yùn)行環(huán)境具體細(xì)節(jié)見下文Jest 配置與運(yùn)行環(huán)境一節(jié)。單元測試的定位與適用場景單元測試最適合不依賴 DOM 或 Electron API 的純函數(shù)與模塊。當(dāng)你修改的代碼符合這一特征時(shí)就應(yīng)當(dāng)考慮為其配套測試。這也正是倉庫中絕大多數(shù)測試的形態(tài)從 app/test/unit 的清單可以看到enum-test.ts、email-test.ts、diff-parser-test.ts、fuzzy-find-test.ts、path-test.ts、status-parser-test.ts等模塊無一例外地對(duì)應(yīng)app/src/下某個(gè)具體源模塊。以 enum-test.ts 為例它直接測試app/src/lib/enum.ts導(dǎo)出的parseEnumValue函數(shù)import { parseEnumValue } from ../../src/lib/enum enum TestEnum { Foo foo, Bar bar is the thing, } describe(parseEnumValue, () { it(parses an enum type from a string, () { expect(parseEnumValue(TestEnum, foo)).toBe(TestEnum.Foo) expect(parseEnumValue(TestEnum, bar is the thing)).toBe(TestEnum.Bar) }) it(returns undefined when enum value doesnt exist, () { expect(parseEnumValue(TestEnum, baz)).toBe(undefined) }) })這個(gè)例子展示了倉庫單元測試的典型特征describe塊對(duì)應(yīng)被測模塊it塊對(duì)應(yīng)一個(gè)具體行為場景斷言使用 Jest 的expect匹配器且測試目標(biāo)始終是單個(gè)函數(shù)。創(chuàng)建新的測試模塊在開始寫測試之前先檢查app/test/unit下是否已有與你所改動(dòng)的區(qū)域?qū)?yīng)的測試模塊。項(xiàng)目約定每個(gè)測試模塊對(duì)應(yīng)一個(gè)具體的應(yīng)用模塊命名規(guī)則為[app-module]-test.ts其中[app-module]是被測應(yīng)用模塊的文件名。如果不存在對(duì)應(yīng)測試模塊則新建一個(gè)同名文件并以如下骨架起步describe(module being tested, () { it(can test some code, () { expect(true).toEqual(false) }) })注意這里的斷言expect(true).toEqual(false)是刻意寫錯(cuò)的當(dāng)你從 shell 運(yùn)行yarn test:unit時(shí)會(huì)看到該用例失敗——這恰恰證明你的新測試文件已被 Jest runner 成功加載并執(zhí)行。確認(rèn)測試確實(shí)在跑之后再把骨架替換成真正有意義的測試邏輯。編寫真實(shí)測試時(shí)可參考現(xiàn)有測試套件如 app/test/unit 下的各類*-test.ts并遵循以下準(zhǔn)則聚焦單一模塊或函數(shù)復(fù)雜冗長的單元測試通常是代碼組織不利于測試、或測試本身管得太多的信號(hào)。遇到這種情況優(yōu)先考慮重構(gòu)被測代碼而不是堆砌測試。覆蓋值得長期守護(hù)的場景優(yōu)先測試那些一旦回歸會(huì)造成實(shí)際傷害的行為這類測試能有效防止你的工作被后續(xù)改動(dòng)意外破壞。保持簡單易讀好的測試本身即是文檔通常不需要代碼注釋來解釋。對(duì)測試編寫不熟悉時(shí)采用 Arrange-Act-Assert 模式起步先準(zhǔn)備輸入與前置狀態(tài)Arrange再執(zhí)行被測代碼Act最后斷言結(jié)果Assert。寫作過程中記得反復(fù)運(yùn)行yarn test:unit驗(yàn)證測試是否符合預(yù)期。特定測試場景AppStore中的狀態(tài)更新單元測試中最棘手的場景之一是應(yīng)用全局狀態(tài)如AppStore的 repository state的更新邏輯——這類邏輯往往隱含著大量上下文與副作用。原文檔給出的核心建議是把復(fù)雜的狀態(tài)更新規(guī)則從AppStore中抽取為獨(dú)立的純函數(shù)。這樣做有三個(gè)關(guān)鍵收益抽取后得到的是無隱式狀態(tài)的純函數(shù)其依賴通過參數(shù)顯式聲明簽名即契約遵循接收當(dāng)前狀態(tài)、產(chǎn)出新狀態(tài)的模式每個(gè)函數(shù)只專注于單一職責(zé)由于函數(shù)只依賴傳入?yún)?shù)測試時(shí)無需搭建龐大的 store 環(huán)境可測性顯著提升。文檔給出的典型案例是updateChangedFiles其源碼位于 app/src/lib/stores/updates/changes-state.ts。該函數(shù)接收當(dāng)前的IChangesState、IStatusResult以及一個(gè)布爾開關(guān)clearPartialState返回一個(gè)描述應(yīng)如何變更狀態(tài)的結(jié)果對(duì)象export function updateChangedFiles( state: IChangesState, status: IStatusResult, clearPartialState: boolean ): ChangedFilesResult { // 以當(dāng)前工作目錄狀態(tài)構(gòu)建 file id - file 的映射 const filesByID new Mapstring, WorkingDirectoryFileChange() state.workingDirectory.files.forEach(f filesByID.set(f.id, f)) // 逐個(gè)文件合并舊選擇狀態(tài)clearPartialState 為 true 時(shí) // 將部分選中的文件重置為全不選中再按路徑不區(qū)分大小寫排序 const mergedFiles status.workingDirectory.files .map(file { const existingFile filesByID.get(file.id) if (existingFile) { if (clearPartialState) { if ( existingFile.selection.getSelectionType() DiffSelectionType.Partial ) { return file.withIncludeAll(false) } } return file.withSelection(existingFile.selection) } else { return file } }) .sort((x, y) caseInsensitiveCompare(x.path, y.path)) // ... 繼續(xù)處理選擇狀態(tài)與 diff 的保留/清理 }關(guān)于返回類型需要說明一點(diǎn)原文檔引用的歷史版本中ChangedFilesResult形如{ workingDirectory, selectedFileIDs, diff }而在當(dāng)前倉庫中該內(nèi)部類型已演化為包含workingDirectory與selection兩個(gè)只讀字段見 changes-state.ts參數(shù)簽名(state, status, clearPartialState)則保持不變。閱讀本文時(shí)請以當(dāng)前源碼為準(zhǔn)。調(diào)用方AppStore內(nèi)部隨后把該結(jié)果合并進(jìn)當(dāng)前狀態(tài)this.repositoryStateCache.updateChangesState(repository, state updateChangedFiles(state, status, clearPartialState) )正是這種純函數(shù) 狀態(tài)合并的模式讓針對(duì)狀態(tài)更新的測試變得非常直接。倉庫中對(duì)應(yīng)的測試模塊為 app/test/unit/stores/updates/update-changed-files-test.ts它利用app/test/helpers/changes-state-helper.ts提供的createState、createStatus工廠構(gòu)造輸入再直接調(diào)用updateChangedFiles并斷言返回的workingDirectory與選擇狀態(tài)import { updateChangedFiles } from ../../../../src/lib/stores/updates/changes-state import { createState, createStatus } from ../../../helpers/changes-state-helper describe(updateChangedFiles, () { it(clears partial selection on file when clearPartialState is true, () { const prevState createState({ workingDirectory: oldWorkingDirectory }) const status createStatus({ workingDirectory: oldWorkingDirectory }) const { workingDirectory } updateChangedFiles(prevState, status, true) const partialFile workingDirectory.findFileWithID(partiallySelectedFile.id) expect(partialFile!.selection.getSelectionType()).toBe( DiffSelectionType.None ) }) })整個(gè)測試過程完全不涉及 AppStore 實(shí)例或 Electron 運(yùn)行時(shí)——這正是抽取純函數(shù)以提升可測性這一原則在倉庫中的直接落地。Jest 配置與運(yùn)行環(huán)境yarn test:unit背后由 app/jest.unit.config.js 驅(qū)動(dòng)的 Jest runner 支撐。理解這份配置有助于你判斷自己的測試文件能否被正確發(fā)現(xiàn)與執(zhí)行module.exports { roots: [rootDir/src/, rootDir/test/], transform: { ^.\\.tsx?$: ts-jest, \\.m?jsx?$: rootDir/test/esm-transformer.js, }, resolver: rootDir/test/resolver.js, testMatch: [**/unit/**/*-test.ts{,x}], moduleFileExtensions: [ts, tsx, js, jsx, json, node], setupFiles: [rootDir/test/globals.ts, rootDir/test/unit-test-env.ts], setupFilesAfterEnv: [rootDir/test/setup-test-framework.ts], reporters: [default, rootDir../script/jest-actions-reporter.js], // For now, github Node modules required to be transformed by jest-esm-transformer transformIgnorePatterns: [node_modules/(?!(github))], testEnvironment: jsdom, }幾個(gè)關(guān)鍵點(diǎn)testMatch只匹配**/unit/**/*-test.ts{,x}即只有unit目錄下以-test.ts/-test.tsx結(jié)尾的文件才會(huì)被當(dāng)作測試運(yùn)行——這解釋了為什么命名規(guī)則如此重要roots同時(shí)包含src/與test/意味著測試可以像上面enum-test.ts那樣通過相對(duì)路徑直接導(dǎo)入被測源碼TypeScript 由ts-jest即時(shí)轉(zhuǎn)換而github命名空間下的 ESM 依賴則交給 esm-transformer.js 處理測試環(huán)境為jsdomglobals.ts 與 unit-test-env.ts 在用例執(zhí)行前完成全局環(huán)境注入setup-test-framework.ts 則在測試框架就緒后執(zhí)行公共初始化。Electron API 的 mock 機(jī)制也是單元測試能夠脫離 Electron 運(yùn)行的關(guān)鍵app/test/__mocks__/electron.ts以jest.fn()為shell、remote、ipcRenderer等模塊提供樁實(shí)現(xiàn)。例如shell.moveItemToTrash被替換為 mockipcRenderer.on/send/invoke亦然這樣被測代碼即便觸碰到 Electron API 也不會(huì)真正去操作系統(tǒng)或渲染進(jìn)程而測試可以通過這些jest.fn()斷言調(diào)用行為。測試的輔助設(shè)施fixtures 與 helpers對(duì)于需要真實(shí) Git 倉庫或更復(fù)雜環(huán)境的測試尤其是app/test/unit/git/下的各模塊如status-test.ts、diff-test.ts、branch-test.ts、log-test.ts等倉庫提供了兩套輔助設(shè)施fixtures預(yù)置的 Git 倉庫與數(shù)據(jù)文件。例如repository-with-105-commits/含 322 個(gè)無擴(kuò)展名對(duì)象文件與配套R(shí)EADME.md、.sh腳本、merge-parser/204 個(gè)解析用.txt文件、test-repo-with-tags/、detached-head/等測試可直接指向這些倉庫執(zhí)行真實(shí)的 git 命令。helpers邏輯復(fù)用層典型如 git.tsgit 命令封裝、temp.ts臨時(shí)目錄管理、repositories.ts、repository-scaffolding.ts倉庫搭建、repository-builder-*系列為分支裁剪、cherry-pick、rebase、pull 等場景生成特定狀態(tài)的倉庫以及databases/、stores/、menus/子目錄下的測試專用基礎(chǔ)設(shè)施。測試組織約定與演進(jìn)方向綜上倉庫的測試約定可以歸納為測試文件一律放在app/test/unit下命名[app-module]-test.ts與app/src中的被測模塊一一對(duì)應(yīng)需要模擬 Electron 時(shí)優(yōu)先復(fù)用/擴(kuò)充app/test/__mocks__/electron.ts需要真實(shí)倉庫或復(fù)雜狀態(tài)時(shí)優(yōu)先復(fù)用fixtures與helpers而不是在測試內(nèi)部臨時(shí)搭建涉及AppStore狀態(tài)更新等復(fù)雜邏輯時(shí)先抽取純函數(shù)再測試具體模式可參照updateChangedFiles與其測試模塊。原文檔同時(shí)指出unit子目錄與app/src布局的對(duì)應(yīng)關(guān)系尚未被嚴(yán)格定義并會(huì)隨源碼布局演進(jìn)計(jì)劃#5645而調(diào)整。這意味著在新增測試目錄時(shí)不必過度糾結(jié)層級(jí)是否與源碼完全鏡像——真正重要的是保持每個(gè)測試模塊對(duì)應(yīng)一個(gè)應(yīng)用模塊的粒度約定讓測試與代碼的映射關(guān)系清晰可循。贊分享開發(fā)工具桌面應(yīng)用【免費(fèi)下載鏈接】desktopFork of GitHub Desktop to support various Linux distributions項(xiàng)目地址https://gitcode.com/gh_mirrors/des/desktop點(diǎn)擊查看免費(fèi)下載相關(guān)推薦Cycle.js遺留代碼單元測試為非響應(yīng)式代碼添加可靠測試Cycle.js遺留代碼單元測試為非響應(yīng)式代碼添加可靠測試 你是否還在為遺留代碼的測試覆蓋率發(fā)愁面對(duì)非響應(yīng)式架構(gòu)的Cycle.js項(xiàng)目如何快速構(gòu)建可靠的測前端Web框架Klavis 中 GitHub MCP Server 的測試體系單元測試、Schema 快照與 E2E 測試實(shí)戰(zhàn)Klavis 中 GitHub MCP Server 的測試體系單元測試、Schema 快照與 E2E 測試實(shí)戰(zhàn) 本文以 Klavis 倉庫中 github_AI 應(yīng)用LLM 網(wǎng)關(guān)MCP 服務(wù)工具調(diào)用KOReader 單元測試指南busted 測試體系與 ./kodev test 實(shí)戰(zhàn)KOReader 單元測試指南busted 測試體系與 ./kodev test 實(shí)戰(zhàn) 本文圍繞 doc/Unit_tests.md https://link桌面應(yīng)用跨平臺(tái)嵌入式上一篇深度解析6自由度KUKA機(jī)械臂自主搬運(yùn)系統(tǒng)從理論到實(shí)踐的完整指南下一篇Mac鼠標(biāo)滾輪卡頓終極解決方案Mos讓外接鼠標(biāo)體驗(yàn)如絲般順滑創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考