[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"knowledge-\u002Fapi\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Ftest":3,"knowledge-related-\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Ftest":2422,"knowledge-series-\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Ftest":2448},{"source":4,"connector":5,"page":6},"sqlite","better-sqlite3",{"path":7,"title":8,"description":9,"category":10,"tags":11,"date":14,"pinned":15,"draft":15,"body":16,"seo":2418,"stem":2419,"id":2420,"extension":2421},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Ftest","测试策略","Vue 应用常用 Vitest + @vue\u002Ftest-utils 做单元\u002F组件测试，Playwright 或 Cypress 做 E2E。见 Vue 基础。","技能小册",[12,13],"vue","test","2024-10-23",false,{"type":17,"value":18,"toc":2410},"minimark",[19,22,37,40,45,48,55,65,100,107,280,291,309,314,330,431,442,445,465,681,688,695,701,709,858,865,988,1007,1011,1014,1233,1244,1248,1251,1255,1258,1280,1285,1432,1435,1659,1663,1666,1685,1688,1693,1701,1705,1708,1711,1731,1734,1754,1763,1882,1885,1889,1892,1895,1922,1925,1942,1945,1969,1973,2191,2196,2216,2222,2225,2331,2338,2342,2345,2406],[20,21,8],"h1",{"id":8},[23,24,25,26,30,31,36],"p",{},"Vue 应用常用 Vitest + ",[27,28,29],"code",{},"@vue\u002Ftest-utils"," 做单元\u002F组件测试，Playwright 或 Cypress 做 E2E。见 ",[32,33,35],"a",{"href":34},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Fbasics","Vue 基础","。",[23,38,39],{},"Vue 应用的测试可分为单元测试、组件测试、端到端（E2E）测试和快照测试。合理的测试策略可以提升代码质量和交付信心，同时避免过度测试带来的维护成本。",[41,42,44],"h3",{"id":43},"一单元测试","一、单元测试",[23,46,47],{},"单元测试针对函数、组合式函数或单个组件进行隔离验证。",[49,50,52,53],"h4",{"id":51},"_1-测试环境搭建vitest-vuetest-utils","1. 测试环境搭建：Vitest + ",[27,54,29],{},[23,56,57,61,62,64],{},[58,59,60],"strong",{},"Vitest"," 是基于 Vite 的单元测试框架，与 Vite 配置无缝集成，速度极快。配合 ",[27,63,29],{},"（Vue 官方测试工具库）可以渲染和交互组件。",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark","npm install -D vitest @vue\u002Ftest-utils jsdom\n","bash","",[27,73,74],{"__ignoreMap":71},[75,76,79,83,87,91,94,97],"span",{"class":77,"line":78},"line",1,[75,80,82],{"class":81},"sbgvK","npm",[75,84,86],{"class":85},"s_sjI"," install",[75,88,90],{"class":89},"stzsN"," -D",[75,92,93],{"class":85}," vitest",[75,95,96],{"class":85}," @vue\u002Ftest-utils",[75,98,99],{"class":85}," jsdom\n",[23,101,102,103,106],{},"配置 ",[27,104,105],{},"vitest.config.ts","：",[66,108,112],{"className":109,"code":110,"language":111,"meta":71,"style":71},"language-ts shiki shiki-themes material-theme-lighter github-light github-dark","import { defineConfig } from \"vitest\u002Fconfig\";\nimport vue from \"@vitejs\u002Fplugin-vue\";\n\nexport default defineConfig({\n  plugins: [vue()],\n  test: {\n    environment: \"jsdom\", \u002F\u002F 模拟浏览器环境\n    globals: true, \u002F\u002F 全局使用 describe\u002Fit\u002Fexpect\n  },\n});\n","ts",[27,113,114,147,167,174,192,213,224,246,263,269],{"__ignoreMap":71},[75,115,116,120,124,128,131,134,138,141,144],{"class":77,"line":78},[75,117,119],{"class":118},"sVHd0","import",[75,121,123],{"class":122},"sP7_E"," {",[75,125,127],{"class":126},"su5hD"," defineConfig",[75,129,130],{"class":122}," }",[75,132,133],{"class":118}," from",[75,135,137],{"class":136},"sjJ54"," \"",[75,139,140],{"class":85},"vitest\u002Fconfig",[75,142,143],{"class":136},"\"",[75,145,146],{"class":122},";\n",[75,148,150,152,155,158,160,163,165],{"class":77,"line":149},2,[75,151,119],{"class":118},[75,153,154],{"class":126}," vue ",[75,156,157],{"class":118},"from",[75,159,137],{"class":136},[75,161,162],{"class":85},"@vitejs\u002Fplugin-vue",[75,164,143],{"class":136},[75,166,146],{"class":122},[75,168,170],{"class":77,"line":169},3,[75,171,173],{"emptyLinePlaceholder":172},true,"\n",[75,175,177,180,183,186,189],{"class":77,"line":176},4,[75,178,179],{"class":118},"export",[75,181,182],{"class":118}," default",[75,184,127],{"class":185},"sGLFI",[75,187,188],{"class":126},"(",[75,190,191],{"class":122},"{\n",[75,193,195,199,202,205,207,210],{"class":77,"line":194},5,[75,196,198],{"class":197},"skxfh","  plugins",[75,200,201],{"class":122},":",[75,203,204],{"class":126}," [",[75,206,12],{"class":185},[75,208,209],{"class":126},"()]",[75,211,212],{"class":122},",\n",[75,214,216,219,221],{"class":77,"line":215},6,[75,217,218],{"class":197},"  test",[75,220,201],{"class":122},[75,222,223],{"class":122}," {\n",[75,225,227,230,232,234,237,239,242],{"class":77,"line":226},7,[75,228,229],{"class":197},"    environment",[75,231,201],{"class":122},[75,233,137],{"class":136},[75,235,236],{"class":85},"jsdom",[75,238,143],{"class":136},[75,240,241],{"class":122},",",[75,243,245],{"class":244},"sutJx"," \u002F\u002F 模拟浏览器环境\n",[75,247,249,252,254,258,260],{"class":77,"line":248},8,[75,250,251],{"class":197},"    globals",[75,253,201],{"class":122},[75,255,257],{"class":256},"syTEX"," true",[75,259,241],{"class":122},[75,261,262],{"class":244}," \u002F\u002F 全局使用 describe\u002Fit\u002Fexpect\n",[75,264,266],{"class":77,"line":265},9,[75,267,268],{"class":122},"  },\n",[75,270,272,275,278],{"class":77,"line":271},10,[75,273,274],{"class":122},"}",[75,276,277],{"class":126},")",[75,279,146],{"class":122},[49,281,283,284,287,288],{"id":282},"_2-组件挂载方法mount-vs-shallowmount","2. 组件挂载方法：",[27,285,286],{},"mount"," vs ",[27,289,290],{},"shallowMount",[292,293,294,302],"ul",{},[295,296,297,301],"li",{},[58,298,299],{},[27,300,286],{},"：完全渲染组件及其所有子组件。适合测试组件之间的交互，但可能因为子组件复杂导致测试缓慢或难以定位问题。",[295,303,304,308],{},[58,305,306],{},[27,307,290],{},"：仅渲染当前组件，子组件以占位符（stub）形式出现。用于隔离当前组件的逻辑，避免子组件的副作用。",[23,310,311,106],{},[58,312,313],{},"选择原则",[292,315,316,321],{},[295,317,318,319],{},"测试组件自身逻辑（事件处理、计算属性、生命周期）→ ",[27,320,290],{},[295,322,323,324,326,327,329],{},"测试组件与其子组件的集成（如插槽内容传递、子组件触发事件） → ",[27,325,286],{}," 或使用 ",[27,328,290],{}," + 手动 stub",[66,331,335],{"className":332,"code":333,"language":334,"meta":71,"style":71},"language-js shiki shiki-themes material-theme-lighter github-light github-dark","import { mount, shallowMount } from \"@vue\u002Ftest-utils\";\nimport Parent from \".\u002FParent.vue\";\n\n\u002F\u002F 浅渲染，子组件会被替换为 \u003Cchild-stub>\nconst wrapper = shallowMount(Parent);\n\u002F\u002F 完全渲染\nconst wrapperFull = mount(Parent);\n","js",[27,336,337,363,381,385,390,411,416],{"__ignoreMap":71},[75,338,339,341,343,346,348,351,353,355,357,359,361],{"class":77,"line":78},[75,340,119],{"class":118},[75,342,123],{"class":122},[75,344,345],{"class":126}," mount",[75,347,241],{"class":122},[75,349,350],{"class":126}," shallowMount",[75,352,130],{"class":122},[75,354,133],{"class":118},[75,356,137],{"class":136},[75,358,29],{"class":85},[75,360,143],{"class":136},[75,362,146],{"class":122},[75,364,365,367,370,372,374,377,379],{"class":77,"line":149},[75,366,119],{"class":118},[75,368,369],{"class":126}," Parent ",[75,371,157],{"class":118},[75,373,137],{"class":136},[75,375,376],{"class":85},".\u002FParent.vue",[75,378,143],{"class":136},[75,380,146],{"class":122},[75,382,383],{"class":77,"line":169},[75,384,173],{"emptyLinePlaceholder":172},[75,386,387],{"class":77,"line":176},[75,388,389],{"class":244},"\u002F\u002F 浅渲染，子组件会被替换为 \u003Cchild-stub>\n",[75,391,392,396,400,404,406,409],{"class":77,"line":194},[75,393,395],{"class":394},"sbsja","const",[75,397,399],{"class":398},"s_hVV"," wrapper",[75,401,403],{"class":402},"smGrS"," =",[75,405,350],{"class":185},[75,407,408],{"class":126},"(Parent)",[75,410,146],{"class":122},[75,412,413],{"class":77,"line":215},[75,414,415],{"class":244},"\u002F\u002F 完全渲染\n",[75,417,418,420,423,425,427,429],{"class":77,"line":226},[75,419,395],{"class":394},[75,421,422],{"class":398}," wrapperFull",[75,424,403],{"class":402},[75,426,345],{"class":185},[75,428,408],{"class":126},[75,430,146],{"class":122},[49,432,434,435,438,439],{"id":433},"_3-异步测试flushpromises-与-nexttick","3. 异步测试：",[27,436,437],{},"flushPromises"," 与 ",[27,440,441],{},"nextTick",[23,443,444],{},"Vue 的响应式更新和 Promise 异步操作需要特殊处理。",[292,446,447,454],{},[295,448,449,453],{},[58,450,451],{},[27,452,441],{},"：等待 Vue 完成 DOM 更新。",[295,455,456,460,461,464],{},[58,457,458],{},[27,459,437],{},"：等待所有微任务（Promise、",[27,462,463],{},"async"," 函数）完成。",[66,466,468],{"className":332,"code":467,"language":334,"meta":71,"style":71},"import { nextTick } from \"vue\";\nimport { flushPromises } from \"@vue\u002Ftest-utils\";\n\ntest(\"异步数据加载\", async () => {\n  const wrapper = mount(AsyncComponent);\n  \u002F\u002F 触发异步操作（比如调用 API）\n  wrapper.find(\"button\").trigger(\"click\");\n  await flushPromises(); \u002F\u002F 等待所有 Promise resolve\n  await nextTick(); \u002F\u002F 等待 DOM 更新\n  expect(wrapper.text()).toContain(\"Loaded\");\n});\n",[27,469,470,491,512,516,542,562,567,607,623,636,672],{"__ignoreMap":71},[75,471,472,474,476,479,481,483,485,487,489],{"class":77,"line":78},[75,473,119],{"class":118},[75,475,123],{"class":122},[75,477,478],{"class":126}," nextTick",[75,480,130],{"class":122},[75,482,133],{"class":118},[75,484,137],{"class":136},[75,486,12],{"class":85},[75,488,143],{"class":136},[75,490,146],{"class":122},[75,492,493,495,497,500,502,504,506,508,510],{"class":77,"line":149},[75,494,119],{"class":118},[75,496,123],{"class":122},[75,498,499],{"class":126}," flushPromises",[75,501,130],{"class":122},[75,503,133],{"class":118},[75,505,137],{"class":136},[75,507,29],{"class":85},[75,509,143],{"class":136},[75,511,146],{"class":122},[75,513,514],{"class":77,"line":169},[75,515,173],{"emptyLinePlaceholder":172},[75,517,518,520,522,524,527,529,531,534,537,540],{"class":77,"line":176},[75,519,13],{"class":185},[75,521,188],{"class":126},[75,523,143],{"class":136},[75,525,526],{"class":85},"异步数据加载",[75,528,143],{"class":136},[75,530,241],{"class":122},[75,532,533],{"class":394}," async",[75,535,536],{"class":122}," ()",[75,538,539],{"class":394}," =>",[75,541,223],{"class":122},[75,543,544,547,549,551,553,555,558,560],{"class":77,"line":194},[75,545,546],{"class":394},"  const",[75,548,399],{"class":398},[75,550,403],{"class":402},[75,552,345],{"class":185},[75,554,188],{"class":197},[75,556,557],{"class":126},"AsyncComponent",[75,559,277],{"class":197},[75,561,146],{"class":122},[75,563,564],{"class":77,"line":215},[75,565,566],{"class":244},"  \u002F\u002F 触发异步操作（比如调用 API）\n",[75,568,569,572,575,578,580,582,585,587,589,591,594,596,598,601,603,605],{"class":77,"line":226},[75,570,571],{"class":126},"  wrapper",[75,573,574],{"class":122},".",[75,576,577],{"class":185},"find",[75,579,188],{"class":197},[75,581,143],{"class":136},[75,583,584],{"class":85},"button",[75,586,143],{"class":136},[75,588,277],{"class":197},[75,590,574],{"class":122},[75,592,593],{"class":185},"trigger",[75,595,188],{"class":197},[75,597,143],{"class":136},[75,599,600],{"class":85},"click",[75,602,143],{"class":136},[75,604,277],{"class":197},[75,606,146],{"class":122},[75,608,609,612,614,617,620],{"class":77,"line":248},[75,610,611],{"class":118},"  await",[75,613,499],{"class":185},[75,615,616],{"class":197},"()",[75,618,619],{"class":122},";",[75,621,622],{"class":244}," \u002F\u002F 等待所有 Promise resolve\n",[75,624,625,627,629,631,633],{"class":77,"line":265},[75,626,611],{"class":118},[75,628,478],{"class":185},[75,630,616],{"class":197},[75,632,619],{"class":122},[75,634,635],{"class":244}," \u002F\u002F 等待 DOM 更新\n",[75,637,638,641,643,646,648,651,654,656,659,661,663,666,668,670],{"class":77,"line":271},[75,639,640],{"class":185},"  expect",[75,642,188],{"class":197},[75,644,645],{"class":126},"wrapper",[75,647,574],{"class":122},[75,649,650],{"class":185},"text",[75,652,653],{"class":197},"())",[75,655,574],{"class":122},[75,657,658],{"class":185},"toContain",[75,660,188],{"class":197},[75,662,143],{"class":136},[75,664,665],{"class":85},"Loaded",[75,667,143],{"class":136},[75,669,277],{"class":197},[75,671,146],{"class":122},[75,673,675,677,679],{"class":77,"line":674},11,[75,676,274],{"class":122},[75,678,277],{"class":126},[75,680,146],{"class":122},[23,682,683,684,687],{},"Vitest 中也可以使用 ",[27,685,686],{},"vi.useFakeTimers()"," 模拟定时器。",[49,689,691,692],{"id":690},"_4-mock-策略vimock","4. Mock 策略：",[27,693,694],{},"vi.mock",[23,696,697,698,700],{},"Vitest 提供 ",[27,699,694],{}," 来模拟模块，可以拦截 API 请求、composable 函数或 Pinia store。",[292,702,703],{},[295,704,705,708],{},[58,706,707],{},"模拟 API 请求","（如 axios 或 fetch）：",[66,710,712],{"className":332,"code":711,"language":334,"meta":71,"style":71},"import { vi } from 'vitest';\nimport axios from 'axios';\nvi.mock('axios');\nconst mockedAxios = axios as vi.Mocked\u003Ctypeof axios>;\nmockedAxios.get.mockResolvedValue({ data: { name: 'Test' } });\n",[27,713,714,738,756,778,811],{"__ignoreMap":71},[75,715,716,718,720,723,725,727,730,733,736],{"class":77,"line":78},[75,717,119],{"class":118},[75,719,123],{"class":122},[75,721,722],{"class":126}," vi",[75,724,130],{"class":122},[75,726,133],{"class":118},[75,728,729],{"class":136}," '",[75,731,732],{"class":85},"vitest",[75,734,735],{"class":136},"'",[75,737,146],{"class":122},[75,739,740,742,745,747,749,752,754],{"class":77,"line":149},[75,741,119],{"class":118},[75,743,744],{"class":126}," axios ",[75,746,157],{"class":118},[75,748,729],{"class":136},[75,750,751],{"class":85},"axios",[75,753,735],{"class":136},[75,755,146],{"class":122},[75,757,758,761,763,766,768,770,772,774,776],{"class":77,"line":169},[75,759,760],{"class":126},"vi",[75,762,574],{"class":122},[75,764,765],{"class":185},"mock",[75,767,188],{"class":126},[75,769,735],{"class":136},[75,771,751],{"class":85},[75,773,735],{"class":136},[75,775,277],{"class":126},[75,777,146],{"class":122},[75,779,780,782,785,787,789,792,794,796,799,802,805,808],{"class":77,"line":176},[75,781,395],{"class":394},[75,783,784],{"class":398}," mockedAxios",[75,786,403],{"class":402},[75,788,744],{"class":126},[75,790,791],{"class":118},"as",[75,793,722],{"class":81},[75,795,574],{"class":122},[75,797,798],{"class":81},"Mocked",[75,800,801],{"class":122},"\u003C",[75,803,804],{"class":402},"typeof",[75,806,807],{"class":126}," axios",[75,809,810],{"class":122},">;\n",[75,812,813,816,818,821,823,826,828,831,834,836,838,841,843,845,848,850,852,854,856],{"class":77,"line":194},[75,814,815],{"class":126},"mockedAxios",[75,817,574],{"class":122},[75,819,820],{"class":126},"get",[75,822,574],{"class":122},[75,824,825],{"class":185},"mockResolvedValue",[75,827,188],{"class":126},[75,829,830],{"class":122},"{",[75,832,833],{"class":197}," data",[75,835,201],{"class":122},[75,837,123],{"class":122},[75,839,840],{"class":197}," name",[75,842,201],{"class":122},[75,844,729],{"class":136},[75,846,847],{"class":85},"Test",[75,849,735],{"class":136},[75,851,130],{"class":122},[75,853,130],{"class":122},[75,855,277],{"class":126},[75,857,146],{"class":122},[292,859,860],{},[295,861,862,106],{},[58,863,864],{},"模拟 composable 函数",[66,866,868],{"className":332,"code":867,"language":334,"meta":71,"style":71},"vi.mock(\"@\u002Fcomposables\u002FuseUser\", () => ({\n  useUser: vi.fn(() => ({\n    user: ref({ name: \"Mocked\" }),\n    isLoggedIn: ref(true),\n  })),\n}));\n",[27,869,870,898,922,952,970,980],{"__ignoreMap":71},[75,871,872,874,876,878,880,882,885,887,889,891,893,896],{"class":77,"line":78},[75,873,760],{"class":126},[75,875,574],{"class":122},[75,877,765],{"class":185},[75,879,188],{"class":126},[75,881,143],{"class":136},[75,883,884],{"class":85},"@\u002Fcomposables\u002FuseUser",[75,886,143],{"class":136},[75,888,241],{"class":122},[75,890,536],{"class":122},[75,892,539],{"class":394},[75,894,895],{"class":126}," (",[75,897,191],{"class":122},[75,899,900,903,905,907,909,912,914,916,918,920],{"class":77,"line":149},[75,901,902],{"class":197},"  useUser",[75,904,201],{"class":122},[75,906,722],{"class":126},[75,908,574],{"class":122},[75,910,911],{"class":185},"fn",[75,913,188],{"class":126},[75,915,616],{"class":122},[75,917,539],{"class":394},[75,919,895],{"class":126},[75,921,191],{"class":122},[75,923,924,927,929,932,934,936,938,940,942,944,946,948,950],{"class":77,"line":169},[75,925,926],{"class":197},"    user",[75,928,201],{"class":122},[75,930,931],{"class":185}," ref",[75,933,188],{"class":126},[75,935,830],{"class":122},[75,937,840],{"class":197},[75,939,201],{"class":122},[75,941,137],{"class":136},[75,943,798],{"class":85},[75,945,143],{"class":136},[75,947,130],{"class":122},[75,949,277],{"class":126},[75,951,212],{"class":122},[75,953,954,957,959,961,963,966,968],{"class":77,"line":176},[75,955,956],{"class":197},"    isLoggedIn",[75,958,201],{"class":122},[75,960,931],{"class":185},[75,962,188],{"class":126},[75,964,965],{"class":256},"true",[75,967,277],{"class":126},[75,969,212],{"class":122},[75,971,972,975,978],{"class":77,"line":194},[75,973,974],{"class":122},"  }",[75,976,977],{"class":126},"))",[75,979,212],{"class":122},[75,981,982,984,986],{"class":77,"line":215},[75,983,274],{"class":122},[75,985,977],{"class":126},[75,987,146],{"class":122},[292,989,990],{},[295,991,992,995,996,999,1000,1003,1004,1006],{},[58,993,994],{},"模拟 Pinia store","：直接使用 ",[27,997,998],{},"createPinia"," + 模拟 ",[27,1001,1002],{},"store"," 的返回值，或者使用 ",[27,1005,694],{}," 模拟整个 store 模块。",[49,1008,1010],{"id":1009},"_5-组合式函数的纯函数测试","5. 组合式函数的纯函数测试",[23,1012,1013],{},"组合式函数（composables）通常不依赖 DOM，可以直接在测试中调用并断言返回值。",[66,1015,1017],{"className":332,"code":1016,"language":334,"meta":71,"style":71},"import { useCounter } from \"@\u002Fcomposables\u002FuseCounter\";\nimport { describe, it, expect } from \"vitest\";\n\ndescribe(\"useCounter\", () => {\n  it(\"should increment count\", () => {\n    const { count, increment } = useCounter(0);\n    expect(count.value).toBe(0);\n    increment();\n    expect(count.value).toBe(1);\n  });\n});\n",[27,1018,1019,1041,1072,1076,1098,1120,1151,1181,1190,1217,1225],{"__ignoreMap":71},[75,1020,1021,1023,1025,1028,1030,1032,1034,1037,1039],{"class":77,"line":78},[75,1022,119],{"class":118},[75,1024,123],{"class":122},[75,1026,1027],{"class":126}," useCounter",[75,1029,130],{"class":122},[75,1031,133],{"class":118},[75,1033,137],{"class":136},[75,1035,1036],{"class":85},"@\u002Fcomposables\u002FuseCounter",[75,1038,143],{"class":136},[75,1040,146],{"class":122},[75,1042,1043,1045,1047,1050,1052,1055,1057,1060,1062,1064,1066,1068,1070],{"class":77,"line":149},[75,1044,119],{"class":118},[75,1046,123],{"class":122},[75,1048,1049],{"class":126}," describe",[75,1051,241],{"class":122},[75,1053,1054],{"class":126}," it",[75,1056,241],{"class":122},[75,1058,1059],{"class":126}," expect",[75,1061,130],{"class":122},[75,1063,133],{"class":118},[75,1065,137],{"class":136},[75,1067,732],{"class":85},[75,1069,143],{"class":136},[75,1071,146],{"class":122},[75,1073,1074],{"class":77,"line":169},[75,1075,173],{"emptyLinePlaceholder":172},[75,1077,1078,1081,1083,1085,1088,1090,1092,1094,1096],{"class":77,"line":176},[75,1079,1080],{"class":185},"describe",[75,1082,188],{"class":126},[75,1084,143],{"class":136},[75,1086,1087],{"class":85},"useCounter",[75,1089,143],{"class":136},[75,1091,241],{"class":122},[75,1093,536],{"class":122},[75,1095,539],{"class":394},[75,1097,223],{"class":122},[75,1099,1100,1103,1105,1107,1110,1112,1114,1116,1118],{"class":77,"line":194},[75,1101,1102],{"class":185},"  it",[75,1104,188],{"class":197},[75,1106,143],{"class":136},[75,1108,1109],{"class":85},"should increment count",[75,1111,143],{"class":136},[75,1113,241],{"class":122},[75,1115,536],{"class":122},[75,1117,539],{"class":394},[75,1119,223],{"class":122},[75,1121,1122,1125,1127,1130,1132,1135,1137,1139,1141,1143,1147,1149],{"class":77,"line":215},[75,1123,1124],{"class":394},"    const",[75,1126,123],{"class":122},[75,1128,1129],{"class":398}," count",[75,1131,241],{"class":122},[75,1133,1134],{"class":398}," increment",[75,1136,130],{"class":122},[75,1138,403],{"class":402},[75,1140,1027],{"class":185},[75,1142,188],{"class":197},[75,1144,1146],{"class":1145},"srdBf","0",[75,1148,277],{"class":197},[75,1150,146],{"class":122},[75,1152,1153,1156,1158,1161,1163,1166,1168,1170,1173,1175,1177,1179],{"class":77,"line":226},[75,1154,1155],{"class":185},"    expect",[75,1157,188],{"class":197},[75,1159,1160],{"class":126},"count",[75,1162,574],{"class":122},[75,1164,1165],{"class":126},"value",[75,1167,277],{"class":197},[75,1169,574],{"class":122},[75,1171,1172],{"class":185},"toBe",[75,1174,188],{"class":197},[75,1176,1146],{"class":1145},[75,1178,277],{"class":197},[75,1180,146],{"class":122},[75,1182,1183,1186,1188],{"class":77,"line":248},[75,1184,1185],{"class":185},"    increment",[75,1187,616],{"class":197},[75,1189,146],{"class":122},[75,1191,1192,1194,1196,1198,1200,1202,1204,1206,1208,1210,1213,1215],{"class":77,"line":265},[75,1193,1155],{"class":185},[75,1195,188],{"class":197},[75,1197,1160],{"class":126},[75,1199,574],{"class":122},[75,1201,1165],{"class":126},[75,1203,277],{"class":197},[75,1205,574],{"class":122},[75,1207,1172],{"class":185},[75,1209,188],{"class":197},[75,1211,1212],{"class":1145},"1",[75,1214,277],{"class":197},[75,1216,146],{"class":122},[75,1218,1219,1221,1223],{"class":77,"line":271},[75,1220,974],{"class":122},[75,1222,277],{"class":197},[75,1224,146],{"class":122},[75,1226,1227,1229,1231],{"class":77,"line":674},[75,1228,274],{"class":122},[75,1230,277],{"class":126},[75,1232,146],{"class":122},[23,1234,1235,1236,1239,1240,1243],{},"需要对响应式行为进行测试时，可以创建 ",[27,1237,1238],{},"Vue"," 组件内部的 ",[27,1241,1242],{},"setup"," 环境，但通常直接调用足矣。",[41,1245,1247],{"id":1246},"二端到端测试e2e","二、端到端测试（E2E）",[23,1249,1250],{},"E2E 测试模拟真实用户在浏览器中操作应用，验证整个系统的集成情况。",[49,1252,1254],{"id":1253},"cypress-与-vue-集成","Cypress 与 Vue 集成",[23,1256,1257],{},"Cypress 是 Vue 社区常用的 E2E 工具，配置简单，并提供可视化运行器。",[66,1259,1261],{"className":68,"code":1260,"language":70,"meta":71,"style":71},"npm install -D cypress @cypress\u002Fvue @cypress\u002Fvite-dev-server\n",[27,1262,1263],{"__ignoreMap":71},[75,1264,1265,1267,1269,1271,1274,1277],{"class":77,"line":78},[75,1266,82],{"class":81},[75,1268,86],{"class":85},[75,1270,90],{"class":89},[75,1272,1273],{"class":85}," cypress",[75,1275,1276],{"class":85}," @cypress\u002Fvue",[75,1278,1279],{"class":85}," @cypress\u002Fvite-dev-server\n",[23,1281,102,1282,106],{},[27,1283,1284],{},"cypress.config.ts",[66,1286,1288],{"className":109,"code":1287,"language":111,"meta":71,"style":71},"import { defineConfig } from \"cypress\";\nexport default defineConfig({\n  component: {\n    devServer: { framework: \"vue\", bundler: \"vite\" },\n  },\n  e2e: {\n    baseUrl: \"http:\u002F\u002Flocalhost:5173\",\n    setupNodeEvents(on, config) {},\n  },\n});\n",[27,1289,1290,1311,1323,1332,1369,1373,1382,1398,1420,1424],{"__ignoreMap":71},[75,1291,1292,1294,1296,1298,1300,1302,1304,1307,1309],{"class":77,"line":78},[75,1293,119],{"class":118},[75,1295,123],{"class":122},[75,1297,127],{"class":126},[75,1299,130],{"class":122},[75,1301,133],{"class":118},[75,1303,137],{"class":136},[75,1305,1306],{"class":85},"cypress",[75,1308,143],{"class":136},[75,1310,146],{"class":122},[75,1312,1313,1315,1317,1319,1321],{"class":77,"line":149},[75,1314,179],{"class":118},[75,1316,182],{"class":118},[75,1318,127],{"class":185},[75,1320,188],{"class":126},[75,1322,191],{"class":122},[75,1324,1325,1328,1330],{"class":77,"line":169},[75,1326,1327],{"class":197},"  component",[75,1329,201],{"class":122},[75,1331,223],{"class":122},[75,1333,1334,1337,1339,1341,1344,1346,1348,1350,1352,1354,1357,1359,1361,1364,1366],{"class":77,"line":176},[75,1335,1336],{"class":197},"    devServer",[75,1338,201],{"class":122},[75,1340,123],{"class":122},[75,1342,1343],{"class":197}," framework",[75,1345,201],{"class":122},[75,1347,137],{"class":136},[75,1349,12],{"class":85},[75,1351,143],{"class":136},[75,1353,241],{"class":122},[75,1355,1356],{"class":197}," bundler",[75,1358,201],{"class":122},[75,1360,137],{"class":136},[75,1362,1363],{"class":85},"vite",[75,1365,143],{"class":136},[75,1367,1368],{"class":122}," },\n",[75,1370,1371],{"class":77,"line":194},[75,1372,268],{"class":122},[75,1374,1375,1378,1380],{"class":77,"line":215},[75,1376,1377],{"class":197},"  e2e",[75,1379,201],{"class":122},[75,1381,223],{"class":122},[75,1383,1384,1387,1389,1391,1394,1396],{"class":77,"line":226},[75,1385,1386],{"class":197},"    baseUrl",[75,1388,201],{"class":122},[75,1390,137],{"class":136},[75,1392,1393],{"class":85},"http:\u002F\u002Flocalhost:5173",[75,1395,143],{"class":136},[75,1397,212],{"class":122},[75,1399,1400,1404,1406,1410,1412,1415,1417],{"class":77,"line":248},[75,1401,1403],{"class":1402},"sVXei","    setupNodeEvents",[75,1405,188],{"class":122},[75,1407,1409],{"class":1408},"s99_P","on",[75,1411,241],{"class":122},[75,1413,1414],{"class":1408}," config",[75,1416,277],{"class":122},[75,1418,1419],{"class":122}," {},\n",[75,1421,1422],{"class":77,"line":265},[75,1423,268],{"class":122},[75,1425,1426,1428,1430],{"class":77,"line":271},[75,1427,274],{"class":122},[75,1429,277],{"class":126},[75,1431,146],{"class":122},[23,1433,1434],{},"编写 E2E 测试（例如登录流程）：",[66,1436,1438],{"className":332,"code":1437,"language":334,"meta":71,"style":71},"describe(\"Login\", () => {\n  it(\"should redirect after successful login\", () => {\n    cy.visit(\"\u002Flogin\");\n    cy.get('[data-test=\"email\"]').type(\"user@example.com\");\n    cy.get('[data-test=\"password\"]').type(\"pass123\");\n    cy.get('[data-test=\"submit\"]').click();\n    cy.url().should(\"include\", \"\u002Fdashboard\");\n  });\n});\n",[27,1439,1440,1461,1482,1505,1542,1578,1605,1643,1651],{"__ignoreMap":71},[75,1441,1442,1444,1446,1448,1451,1453,1455,1457,1459],{"class":77,"line":78},[75,1443,1080],{"class":185},[75,1445,188],{"class":126},[75,1447,143],{"class":136},[75,1449,1450],{"class":85},"Login",[75,1452,143],{"class":136},[75,1454,241],{"class":122},[75,1456,536],{"class":122},[75,1458,539],{"class":394},[75,1460,223],{"class":122},[75,1462,1463,1465,1467,1469,1472,1474,1476,1478,1480],{"class":77,"line":149},[75,1464,1102],{"class":185},[75,1466,188],{"class":197},[75,1468,143],{"class":136},[75,1470,1471],{"class":85},"should redirect after successful login",[75,1473,143],{"class":136},[75,1475,241],{"class":122},[75,1477,536],{"class":122},[75,1479,539],{"class":394},[75,1481,223],{"class":122},[75,1483,1484,1487,1489,1492,1494,1496,1499,1501,1503],{"class":77,"line":169},[75,1485,1486],{"class":126},"    cy",[75,1488,574],{"class":122},[75,1490,1491],{"class":185},"visit",[75,1493,188],{"class":197},[75,1495,143],{"class":136},[75,1497,1498],{"class":85},"\u002Flogin",[75,1500,143],{"class":136},[75,1502,277],{"class":197},[75,1504,146],{"class":122},[75,1506,1507,1509,1511,1513,1515,1517,1520,1522,1524,1526,1529,1531,1533,1536,1538,1540],{"class":77,"line":176},[75,1508,1486],{"class":126},[75,1510,574],{"class":122},[75,1512,820],{"class":185},[75,1514,188],{"class":197},[75,1516,735],{"class":136},[75,1518,1519],{"class":85},"[data-test=\"email\"]",[75,1521,735],{"class":136},[75,1523,277],{"class":197},[75,1525,574],{"class":122},[75,1527,1528],{"class":185},"type",[75,1530,188],{"class":197},[75,1532,143],{"class":136},[75,1534,1535],{"class":85},"user@example.com",[75,1537,143],{"class":136},[75,1539,277],{"class":197},[75,1541,146],{"class":122},[75,1543,1544,1546,1548,1550,1552,1554,1557,1559,1561,1563,1565,1567,1569,1572,1574,1576],{"class":77,"line":194},[75,1545,1486],{"class":126},[75,1547,574],{"class":122},[75,1549,820],{"class":185},[75,1551,188],{"class":197},[75,1553,735],{"class":136},[75,1555,1556],{"class":85},"[data-test=\"password\"]",[75,1558,735],{"class":136},[75,1560,277],{"class":197},[75,1562,574],{"class":122},[75,1564,1528],{"class":185},[75,1566,188],{"class":197},[75,1568,143],{"class":136},[75,1570,1571],{"class":85},"pass123",[75,1573,143],{"class":136},[75,1575,277],{"class":197},[75,1577,146],{"class":122},[75,1579,1580,1582,1584,1586,1588,1590,1593,1595,1597,1599,1601,1603],{"class":77,"line":215},[75,1581,1486],{"class":126},[75,1583,574],{"class":122},[75,1585,820],{"class":185},[75,1587,188],{"class":197},[75,1589,735],{"class":136},[75,1591,1592],{"class":85},"[data-test=\"submit\"]",[75,1594,735],{"class":136},[75,1596,277],{"class":197},[75,1598,574],{"class":122},[75,1600,600],{"class":185},[75,1602,616],{"class":197},[75,1604,146],{"class":122},[75,1606,1607,1609,1611,1614,1616,1618,1621,1623,1625,1628,1630,1632,1634,1637,1639,1641],{"class":77,"line":226},[75,1608,1486],{"class":126},[75,1610,574],{"class":122},[75,1612,1613],{"class":185},"url",[75,1615,616],{"class":197},[75,1617,574],{"class":122},[75,1619,1620],{"class":185},"should",[75,1622,188],{"class":197},[75,1624,143],{"class":136},[75,1626,1627],{"class":85},"include",[75,1629,143],{"class":136},[75,1631,241],{"class":122},[75,1633,137],{"class":136},[75,1635,1636],{"class":85},"\u002Fdashboard",[75,1638,143],{"class":136},[75,1640,277],{"class":197},[75,1642,146],{"class":122},[75,1644,1645,1647,1649],{"class":77,"line":248},[75,1646,974],{"class":122},[75,1648,277],{"class":197},[75,1650,146],{"class":122},[75,1652,1653,1655,1657],{"class":77,"line":265},[75,1654,274],{"class":122},[75,1656,277],{"class":126},[75,1658,146],{"class":122},[49,1660,1662],{"id":1661},"playwright-与-vue-集成","Playwright 与 Vue 集成",[23,1664,1665],{},"Playwright 是 Cypress 的有力竞争者，支持多浏览器和更快的并行执行。",[66,1667,1669],{"className":68,"code":1668,"language":70,"meta":71,"style":71},"npm install -D @playwright\u002Ftest @playwright\u002Fexperimental-ct-vue\n",[27,1670,1671],{"__ignoreMap":71},[75,1672,1673,1675,1677,1679,1682],{"class":77,"line":78},[75,1674,82],{"class":81},[75,1676,86],{"class":85},[75,1678,90],{"class":89},[75,1680,1681],{"class":85}," @playwright\u002Ftest",[75,1683,1684],{"class":85}," @playwright\u002Fexperimental-ct-vue\n",[23,1686,1687],{},"配置和使用类似 Cypress，但 API 更为现代。",[23,1689,1690,106],{},[58,1691,1692],{},"选择建议",[292,1694,1695,1698],{},[295,1696,1697],{},"项目复杂度高、需要跨浏览器测试 → Playwright",[295,1699,1700],{},"团队熟悉 JavaScript、希望快速上手 → Cypress",[41,1702,1704],{"id":1703},"三快照测试","三、快照测试",[23,1706,1707],{},"快照测试将组件的渲染输出（HTML 结构）序列化为字符串并与存储的基准快照进行比对，用于捕获意外变更。",[49,1709,1710],{"id":1710},"何时使用",[292,1712,1713,1719,1725],{},[295,1714,1715,1718],{},[58,1716,1717],{},"静态内容组件","：如纯展示的 Header、Footer，内容几乎不变。",[295,1720,1721,1724],{},[58,1722,1723],{},"样式回归测试","：结合 CSS-in-JS 或 CSS Modules，验证样式是否被意外破坏。",[295,1726,1727,1730],{},[58,1728,1729],{},"API 响应数据快照","：测试序列化数据格式。",[49,1732,1733],{"id":1733},"何时避免",[292,1735,1736,1742,1748],{},[295,1737,1738,1741],{},[58,1739,1740],{},"UI 组件频繁变动","：每次需求迭代都需要更新快照，导致测试失效快，维护成本高。",[295,1743,1744,1747],{},[58,1745,1746],{},"动态内容多","：如包含时间戳、随机 ID 的组件，快照会频繁失败。",[295,1749,1750,1753],{},[58,1751,1752],{},"跨平台差异","：不同操作系统渲染的 HTML 可能有细微差别。",[23,1755,1756,1759,1760,1762],{},[58,1757,1758],{},"使用示例","（Vitest + ",[27,1761,29],{},"）：",[66,1764,1766],{"className":332,"code":1765,"language":334,"meta":71,"style":71},"import { mount } from \"@vue\u002Ftest-utils\";\nimport StaticBanner from \".\u002FStaticBanner.vue\";\n\ntest(\"StaticBanner snapshot\", () => {\n  const wrapper = mount(StaticBanner);\n  expect(wrapper.html()).toMatchSnapshot();\n});\n",[27,1767,1768,1788,1806,1810,1831,1850,1874],{"__ignoreMap":71},[75,1769,1770,1772,1774,1776,1778,1780,1782,1784,1786],{"class":77,"line":78},[75,1771,119],{"class":118},[75,1773,123],{"class":122},[75,1775,345],{"class":126},[75,1777,130],{"class":122},[75,1779,133],{"class":118},[75,1781,137],{"class":136},[75,1783,29],{"class":85},[75,1785,143],{"class":136},[75,1787,146],{"class":122},[75,1789,1790,1792,1795,1797,1799,1802,1804],{"class":77,"line":149},[75,1791,119],{"class":118},[75,1793,1794],{"class":126}," StaticBanner ",[75,1796,157],{"class":118},[75,1798,137],{"class":136},[75,1800,1801],{"class":85},".\u002FStaticBanner.vue",[75,1803,143],{"class":136},[75,1805,146],{"class":122},[75,1807,1808],{"class":77,"line":169},[75,1809,173],{"emptyLinePlaceholder":172},[75,1811,1812,1814,1816,1818,1821,1823,1825,1827,1829],{"class":77,"line":176},[75,1813,13],{"class":185},[75,1815,188],{"class":126},[75,1817,143],{"class":136},[75,1819,1820],{"class":85},"StaticBanner snapshot",[75,1822,143],{"class":136},[75,1824,241],{"class":122},[75,1826,536],{"class":122},[75,1828,539],{"class":394},[75,1830,223],{"class":122},[75,1832,1833,1835,1837,1839,1841,1843,1846,1848],{"class":77,"line":194},[75,1834,546],{"class":394},[75,1836,399],{"class":398},[75,1838,403],{"class":402},[75,1840,345],{"class":185},[75,1842,188],{"class":197},[75,1844,1845],{"class":126},"StaticBanner",[75,1847,277],{"class":197},[75,1849,146],{"class":122},[75,1851,1852,1854,1856,1858,1860,1863,1865,1867,1870,1872],{"class":77,"line":215},[75,1853,640],{"class":185},[75,1855,188],{"class":197},[75,1857,645],{"class":126},[75,1859,574],{"class":122},[75,1861,1862],{"class":185},"html",[75,1864,653],{"class":197},[75,1866,574],{"class":122},[75,1868,1869],{"class":185},"toMatchSnapshot",[75,1871,616],{"class":197},[75,1873,146],{"class":122},[75,1875,1876,1878,1880],{"class":77,"line":226},[75,1877,274],{"class":122},[75,1879,277],{"class":126},[75,1881,146],{"class":122},[23,1883,1884],{},"最佳实践：只对关键的代表性组件进行快照测试，而不是所有组件。快照文件应纳入版本控制并定期审查。",[41,1886,1888],{"id":1887},"四测试覆盖率策略","四、测试覆盖率策略",[23,1890,1891],{},"测试覆盖率是衡量测试完整性的指标，但追求 100% 覆盖率往往得不偿失。以下为推荐的优先级：",[49,1893,1894],{"id":1894},"重点关注对象",[1896,1897,1898,1904,1910,1916],"ol",{},[295,1899,1900,1903],{},[58,1901,1902],{},"组合式函数（composables）","：因为它们是可复用的业务逻辑，错误影响面大。",[295,1905,1906,1909],{},[58,1907,1908],{},"工具函数（utils）","：纯函数，容易测试，且通常是错误高发区域。",[295,1911,1912,1915],{},[58,1913,1914],{},"复杂计算属性（computed）","：包含条件判断或数据转换。",[295,1917,1918,1921],{},[58,1919,1920],{},"关键用户流程对应的组件","：如支付表单、登录注册模块。",[49,1923,1924],{"id":1924},"适当覆盖",[292,1926,1927,1936],{},[295,1928,1929,1932,1933,1935],{},[58,1930,1931],{},"UI 交互逻辑","：按钮点击、表单提交、错误提示等，可使用 ",[27,1934,29],{}," 进行交互测试。",[295,1937,1938,1941],{},[58,1939,1940],{},"边界条件","：如输入校验、空数据、加载状态。",[49,1943,1944],{"id":1944},"无需过度追求",[292,1946,1947,1957,1963],{},[295,1948,1949,1952,1953,1956],{},[58,1950,1951],{},"简单的模板逻辑","：如单纯的 ",[27,1954,1955],{},"v-for"," 渲染列表（除非包含复杂过滤）。",[295,1958,1959,1962],{},[58,1960,1961],{},"第三方库的封装","：假定库本身已测试。",[295,1964,1965,1968],{},[58,1966,1967],{},"样式细节","：样式应由视觉回归工具（如 Percy）或人工验收。",[49,1970,1972],{"id":1971},"配置覆盖率工具vitest","配置覆盖率工具（Vitest）",[66,1974,1976],{"className":109,"code":1975,"language":111,"meta":71,"style":71},"\u002F\u002F vitest.config.ts\nexport default defineConfig({\n  test: {\n    coverage: {\n      provider: \"v8\", \u002F\u002F 或 'istanbul'\n      reporter: [\"text\", \"html\"],\n      include: [\"src\u002F**\u002F*.{vue,ts}\"],\n      exclude: [\"src\u002Fmain.ts\", \"src\u002F**\u002F*.d.ts\"],\n      thresholds: {\n        statements: 80,\n        branches: 70,\n        functions: 80,\n        lines: 80,\n      },\n    },\n  },\n});\n",[27,1977,1978,1983,1995,2003,2012,2031,2059,2079,2108,2117,2129,2141,2153,2165,2171,2177,2182],{"__ignoreMap":71},[75,1979,1980],{"class":77,"line":78},[75,1981,1982],{"class":244},"\u002F\u002F vitest.config.ts\n",[75,1984,1985,1987,1989,1991,1993],{"class":77,"line":149},[75,1986,179],{"class":118},[75,1988,182],{"class":118},[75,1990,127],{"class":185},[75,1992,188],{"class":126},[75,1994,191],{"class":122},[75,1996,1997,1999,2001],{"class":77,"line":169},[75,1998,218],{"class":197},[75,2000,201],{"class":122},[75,2002,223],{"class":122},[75,2004,2005,2008,2010],{"class":77,"line":176},[75,2006,2007],{"class":197},"    coverage",[75,2009,201],{"class":122},[75,2011,223],{"class":122},[75,2013,2014,2017,2019,2021,2024,2026,2028],{"class":77,"line":194},[75,2015,2016],{"class":197},"      provider",[75,2018,201],{"class":122},[75,2020,137],{"class":136},[75,2022,2023],{"class":85},"v8",[75,2025,143],{"class":136},[75,2027,241],{"class":122},[75,2029,2030],{"class":244}," \u002F\u002F 或 'istanbul'\n",[75,2032,2033,2036,2038,2040,2042,2044,2046,2048,2050,2052,2054,2057],{"class":77,"line":215},[75,2034,2035],{"class":197},"      reporter",[75,2037,201],{"class":122},[75,2039,204],{"class":126},[75,2041,143],{"class":136},[75,2043,650],{"class":85},[75,2045,143],{"class":136},[75,2047,241],{"class":122},[75,2049,137],{"class":136},[75,2051,1862],{"class":85},[75,2053,143],{"class":136},[75,2055,2056],{"class":126},"]",[75,2058,212],{"class":122},[75,2060,2061,2064,2066,2068,2070,2073,2075,2077],{"class":77,"line":226},[75,2062,2063],{"class":197},"      include",[75,2065,201],{"class":122},[75,2067,204],{"class":126},[75,2069,143],{"class":136},[75,2071,2072],{"class":85},"src\u002F**\u002F*.{vue,ts}",[75,2074,143],{"class":136},[75,2076,2056],{"class":126},[75,2078,212],{"class":122},[75,2080,2081,2084,2086,2088,2090,2093,2095,2097,2099,2102,2104,2106],{"class":77,"line":248},[75,2082,2083],{"class":197},"      exclude",[75,2085,201],{"class":122},[75,2087,204],{"class":126},[75,2089,143],{"class":136},[75,2091,2092],{"class":85},"src\u002Fmain.ts",[75,2094,143],{"class":136},[75,2096,241],{"class":122},[75,2098,137],{"class":136},[75,2100,2101],{"class":85},"src\u002F**\u002F*.d.ts",[75,2103,143],{"class":136},[75,2105,2056],{"class":126},[75,2107,212],{"class":122},[75,2109,2110,2113,2115],{"class":77,"line":265},[75,2111,2112],{"class":197},"      thresholds",[75,2114,201],{"class":122},[75,2116,223],{"class":122},[75,2118,2119,2122,2124,2127],{"class":77,"line":271},[75,2120,2121],{"class":197},"        statements",[75,2123,201],{"class":122},[75,2125,2126],{"class":1145}," 80",[75,2128,212],{"class":122},[75,2130,2131,2134,2136,2139],{"class":77,"line":674},[75,2132,2133],{"class":197},"        branches",[75,2135,201],{"class":122},[75,2137,2138],{"class":1145}," 70",[75,2140,212],{"class":122},[75,2142,2144,2147,2149,2151],{"class":77,"line":2143},12,[75,2145,2146],{"class":197},"        functions",[75,2148,201],{"class":122},[75,2150,2126],{"class":1145},[75,2152,212],{"class":122},[75,2154,2156,2159,2161,2163],{"class":77,"line":2155},13,[75,2157,2158],{"class":197},"        lines",[75,2160,201],{"class":122},[75,2162,2126],{"class":1145},[75,2164,212],{"class":122},[75,2166,2168],{"class":77,"line":2167},14,[75,2169,2170],{"class":122},"      },\n",[75,2172,2174],{"class":77,"line":2173},15,[75,2175,2176],{"class":122},"    },\n",[75,2178,2180],{"class":77,"line":2179},16,[75,2181,268],{"class":122},[75,2183,2185,2187,2189],{"class":77,"line":2184},17,[75,2186,274],{"class":122},[75,2188,277],{"class":126},[75,2190,146],{"class":122},[23,2192,2193,106],{},[58,2194,2195],{},"指标解读",[292,2197,2198,2204,2210],{},[295,2199,2200,2203],{},[58,2201,2202],{},"行覆盖率","：最容易达到，但不能保证逻辑正确。",[295,2205,2206,2209],{},[58,2207,2208],{},"分支覆盖率","：更能发现边缘条件遗漏。",[295,2211,2212,2215],{},[58,2213,2214],{},"函数覆盖率","：确保关键函数被调用。",[2217,2218,2219],"blockquote",{},[23,2220,2221],{},"注意：不要盲目追求覆盖率数字，应在 CI 中设置合理的阈值（如 70-80%），并允许团队根据模块重要性调整。",[41,2223,2224],{"id":2224},"总结",[2226,2227,2228,2247],"table",{},[2229,2230,2231],"thead",{},[2232,2233,2234,2238,2241,2244],"tr",{},[2235,2236,2237],"th",{},"测试类型",[2235,2239,2240],{},"工具\u002F库",[2235,2242,2243],{},"核心关注点",[2235,2245,2246],{},"维护成本",[2248,2249,2250,2267,2283,2299,2315],"tbody",{},[2232,2251,2252,2258,2261,2264],{},[2253,2254,2255],"td",{},[58,2256,2257],{},"单元测试",[2253,2259,2260],{},"Vitest + @vue\u002Ftest-utils",[2253,2262,2263],{},"组合式函数、工具函数、组件逻辑",[2253,2265,2266],{},"低",[2232,2268,2269,2274,2277,2280],{},[2253,2270,2271],{},[58,2272,2273],{},"组件测试",[2253,2275,2276],{},"@vue\u002Ftest-utils (mount\u002Fshallow)",[2253,2278,2279],{},"组件交互、props\u002Femit 验证",[2253,2281,2282],{},"中",[2232,2284,2285,2290,2293,2296],{},[2253,2286,2287],{},[58,2288,2289],{},"E2E 测试",[2253,2291,2292],{},"Cypress \u002F Playwright",[2253,2294,2295],{},"完整用户旅程、跨页面集成",[2253,2297,2298],{},"高",[2232,2300,2301,2306,2309,2312],{},[2253,2302,2303],{},[58,2304,2305],{},"快照测试",[2253,2307,2308],{},"Vitest (toMatchSnapshot)",[2253,2310,2311],{},"静态输出、防意外回归",[2253,2313,2314],{},"低 (但易产生噪音)",[2232,2316,2317,2322,2325,2328],{},[2253,2318,2319],{},[58,2320,2321],{},"覆盖率",[2253,2323,2324],{},"Vitest \u002F Jest 覆盖率报告",[2253,2326,2327],{},"量化测试充分性，指导补充测试",[2253,2329,2330],{},"自动",[23,2332,2333,2334,2337],{},"在实际项目中，推荐采用",[58,2335,2336],{},"测试金字塔","模型：大量单元测试（底层） + 适量组件测试（中间层） + 少量 E2E 测试（顶层）。将测试重点放在稳定且关键的业务逻辑上，可以减少维护负担，同时保证交付质量。",[2339,2340,2341],"h2",{"id":2341},"参考文献",[23,2343,2344],{},"以下链接在编写时均可正常访问：",[2226,2346,2347,2357],{},[2229,2348,2349],{},[2232,2350,2351,2354],{},[2235,2352,2353],{},"资料",[2235,2355,2356],{},"说明",[2248,2358,2359,2372,2383,2394],{},[2232,2360,2361,2369],{},[2253,2362,2363],{},[32,2364,2368],{"href":2365,"rel":2366},"https:\u002F\u002Fcn.vuejs.org\u002Fguide\u002Fscaling-up\u002Ftesting.html",[2367],"nofollow","Vue：测试",[2253,2370,2371],{},"官方测试指南",[2232,2373,2374,2380],{},[2253,2375,2376],{},[32,2377,60],{"href":2378,"rel":2379},"https:\u002F\u002Fvitest.dev\u002F",[2367],[2253,2381,2382],{},"单元测试框架",[2232,2384,2385,2391],{},[2253,2386,2387],{},[32,2388,29],{"href":2389,"rel":2390},"https:\u002F\u002Ftest-utils.vuejs.org\u002F",[2367],[2253,2392,2393],{},"组件测试工具",[2232,2395,2396,2403],{},[2253,2397,2398],{},[32,2399,2402],{"href":2400,"rel":2401},"https:\u002F\u002Fplaywright.dev\u002F",[2367],"Playwright",[2253,2404,2405],{},"E2E（英文）",[2407,2408,2409],"style",{},"html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .stzsN, html code.shiki .stzsN{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .syTEX, html code.shiki .syTEX{--shiki-light:#FF5370;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVXei, html code.shiki .sVXei{--shiki-light:#E53935;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}",{"title":71,"searchDepth":149,"depth":149,"links":2411},[2412,2413,2414,2415,2416,2417],{"id":43,"depth":169,"text":44},{"id":1246,"depth":169,"text":1247},{"id":1703,"depth":169,"text":1704},{"id":1887,"depth":169,"text":1888},{"id":2224,"depth":169,"text":2224},{"id":2341,"depth":149,"text":2341},{"title":8,"description":9},"knowledge\u002Fbooklet\u002Fframe\u002FVue\u002Ftest","knowledge\u002Fknowledge\u002Fbooklet\u002Fframe\u002FVue\u002Ftest.md","md",{"items":2423},[2424,2428,2432,2436,2440,2444],{"path":2425,"title":2426,"description":2427,"category":10,"score":215},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Fperformance\u002Fassets","资源优化","图片懒加载、按需引入与 Web Vitals 监控。见 HTML 图片、CSS 字体优化。",{"path":2429,"title":2430,"description":2431,"category":10,"score":215},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Fstatus","状态管理：Pinia 与 Vuex","Pinia 是 Vue 官方推荐的状态管理库（Vue 3 新项目默认选型）。Vuex 4 仍可与 Vue 3 配合，用于维护旧项目。见 Vue 基础。",{"path":2433,"title":2434,"description":2435,"category":10,"score":215},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Fecology","Vue 生态关键库深度集成","VueUse、Vite 与 Nuxt 是 Vue 3 生态中常用的效率与工程化方案。概览见 Vue 基础。",{"path":2437,"title":2438,"description":2439,"category":10,"score":215},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Freactivity\u002Ftrack","依赖收集与派发更新机制","说明 Vue 3 中 track \u002F trigger 与 targetMap 的工作方式。前置：v2-v3 差异。",{"path":2441,"title":2442,"description":2443,"category":10,"score":215},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Fdesign","组合式 API 与逻辑复用设计模式","组合式 API 与 Composables 是 Vue 3 推荐的逻辑组织方式。概览见 Vue 基础。",{"path":2445,"title":2446,"description":2447,"category":10,"score":215},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Ftypescript","TypeScript 深度集成","Vue 3 源码使用 TypeScript 编写， 可获得良好的类型推导。见 Vue 基础。",{"series":2449,"title":12,"items":2450,"index":2143,"prev":2498,"next":2499},"booklet\u002Fframe\u002Fvue",[2451,2453,2455,2459,2461,2463,2467,2471,2475,2479,2483,2487,2489,2490,2492,2494],{"path":2425,"title":2426,"date":2452,"pinned":15},"2025-11-06",{"path":34,"title":35,"date":2454,"pinned":15},"2021-07-29",{"path":2456,"title":2457,"date":2458,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Fperformance\u002Fbuild","编译与打包优化","2021-08-02",{"path":2441,"title":2442,"date":2460,"pinned":15},"2024-06-16",{"path":2433,"title":2434,"date":2462,"pinned":15},"2025-08-26",{"path":2464,"title":2465,"date":2466,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Fproject","工程化实践","2021-07-20",{"path":2468,"title":2469,"date":2470,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Fprops","组件通信与跨层级设计模式","2023-03-23",{"path":2472,"title":2473,"date":2474,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Freactivity\u002Fproxy","响应式进阶","2023-03-21",{"path":2476,"title":2477,"date":2478,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Frender","渲染机制与编译原理","2021-09-23",{"path":2480,"title":2481,"date":2482,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Frouter","路由与权限控制","2021-09-12",{"path":2484,"title":2485,"date":2486,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Fperformance\u002Fruntime","运行时优化","2021-06-10",{"path":2429,"title":2430,"date":2488,"pinned":15},"2025-10-11",{"path":7,"title":8,"date":14,"pinned":15},{"path":2437,"title":2438,"date":2491,"pinned":15},"2025-01-13",{"path":2445,"title":2446,"date":2493,"pinned":15},"2023-11-29",{"path":2495,"title":2496,"date":2497,"pinned":15},"\u002Fknowledge\u002Fbooklet\u002Fframe\u002Fvue\u002Freactivity\u002Fv2-v3","Vue 2 与 Vue 3 响应式差异","2022-07-06",{"path":2429,"title":2430,"date":2488,"pinned":15},{"path":2437,"title":2438,"date":2491,"pinned":15}]