現(xiàn)自動(dòng)化測(cè)試:settings.json 骨架與驗(yàn)證動(dòng)作)
1. 為什么要在 CI 里讓 DeepSeek 寫(xiě)測(cè)試用例自動(dòng)化測(cè)試最費(fèi)時(shí)間的環(huán)節(jié)往往不是跑用例而是寫(xiě)用例。接口改一個(gè)字段斷言要跟著改新加一個(gè)業(yè)務(wù)分支就得補(bǔ)一組參數(shù)化數(shù)據(jù)。DeepSeek 這類(lèi)模型擅長(zhǎng)把「自然語(yǔ)言描述的需求」翻譯成 pytest 代碼正好補(bǔ)上這塊人力。但直接把模型塞進(jìn) CI 有兩個(gè)現(xiàn)實(shí)問(wèn)題一是 Key 散落在各個(gè)倉(cāng)庫(kù)的 secrets 里輪換一次要改十幾個(gè)地方二是不同模型、不同供應(yīng)商的 Base URL 和鑒權(quán)格式不統(tǒng)一腳本里到處是 if-else。我這次要搭的閉環(huán)是CI 流水線(xiàn)觸發(fā) → 調(diào)用 DeepSeek 根據(jù)接口描述生成測(cè)試用例 → 寫(xiě)入臨時(shí)測(cè)試文件 → 執(zhí)行冒煙腳本 → 斷言通過(guò)后歸檔報(bào)告。整條鏈路里模型調(diào)用統(tǒng)一走 TaoToken 的 API 通道Key 只維護(hù)一份settings.json 作為配置骨架被腳本讀取。這樣換模型、換供應(yīng)商只改配置不動(dòng)測(cè)試代碼。適合誰(shuí)看已經(jīng)在用 GitHub Actions / GitLab CI 跑 pytest想讓模型參與用例生成又不想把鑒權(quán)邏輯寫(xiě)死的測(cè)試或運(yùn)維同學(xué)。下面從配置骨架開(kāi)始一步步把可運(yùn)行的驗(yàn)證動(dòng)作做出來(lái)。2. TaoToken 前置統(tǒng)一 Key 與 API 通道TaoToken 在這里扮演的角色是「統(tǒng)一入口」它提供 OpenAI 兼容的 API 格式DeepSeek 的模型通過(guò)它調(diào)用Base URL 和 Key 都收斂到一處。你不需要在 CI 里分別配置 DeepSeek 官方地址和 TaoToken 地址腳本只認(rèn)一個(gè)base_url。先拿到 Key。訪(fǎng)問(wèn)控制臺(tái)創(chuàng)建 API Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite創(chuàng)建后復(fù)制以sk-開(kāi)頭的字符串存到 CI 的 Secret 里命名建議TAOTOKEN_API_KEY。本地調(diào)試可以放到.env但別提交。API 通道地址是https://taotoken.net/api兼容 OpenAI 的/v1/chat/completions路徑。也就是說(shuō)任何支持自定義 Base URL 的 OpenAI SDK 都能直接指向它。模型名填deepseek-chat即可這是 DeepSeek 的對(duì)話(huà)模型標(biāo)識(shí)。如果你更習(xí)慣用現(xiàn)成的編碼 Agent 來(lái)生成測(cè)試可以看 Coding Plan 的接入方式它把模型調(diào)用和代碼生成串好了https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite接入文檔里有各語(yǔ)言 SDK 的 Base URL 填法遇到 401 或路徑 404 時(shí)對(duì)照排查https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意Key 只放環(huán)境變量不要寫(xiě)進(jìn) settings.json 提交到倉(cāng)庫(kù)。settings.json 里用占位符引用環(huán)境變量名。3. 可復(fù)制的 settings.json 配置骨架settings.json 的定位是「測(cè)試生成器的配置中心」模型參數(shù)、輸出路徑、超時(shí)、重試都在這里腳本讀它CI 也讀它。下面這份骨架可以直接復(fù)制字段按注釋替換。{ llm: { provider: taotoken, base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, model: deepseek-chat, temperature: 0.2, max_tokens: 2048, timeout_seconds: 60, max_retries: 3 }, generation: { prompt_template: prompts/gen_tests.md, output_dir: tests/generated, overwrite: true, language: python, framework: pytest }, smoke: { target_file: tests/generated/test_smoke_generated.py, run_command: pytest -q tests/generated/test_smoke_generated.py, report_path: reports/smoke.xml } }幾個(gè)關(guān)鍵點(diǎn)說(shuō)明。temperature設(shè) 0.2 是為了讓生成的測(cè)試代碼穩(wěn)定別太發(fā)散max_retries配合下面的腳本做指數(shù)退避CI 網(wǎng)絡(luò)抖動(dòng)時(shí)不至于直接失敗。prompt_template指向一個(gè) Markdown 文件里面寫(xiě)清楚「根據(jù)以下接口描述生成 pytest 用例必須包含斷言」這類(lèi)約束比把提示詞硬編碼在腳本里好維護(hù)。output_dir和target_file分開(kāi)是為了讓生成的文件名可預(yù)測(cè)冒煙腳本能直接定位。report_path用 JUnit XML 格式CI 平臺(tái)大多能直接解析。讀取配置的 Python 片段import json import os def load_settings(pathsettings.json): with open(path, r, encodingutf-8) as f: cfg json.load(f) api_key os.environ.get(cfg[llm][api_key_env]) if not api_key: raise RuntimeError(f缺少環(huán)境變量 {cfg[llm][api_key_env]}) cfg[llm][api_key] api_key return cfg這樣 Key 從環(huán)境變量注入settings.json 本身可以安全地進(jìn)版本庫(kù)。4. 生成到斷言的完整驗(yàn)證動(dòng)作先寫(xiě)提示詞模板prompts/gen_tests.md把接口描述和輸出要求講清楚你是一個(gè)測(cè)試工程師。根據(jù)下面的接口描述生成 pytest 測(cè)試用例。 要求 1. 使用 requests 庫(kù)調(diào)用接口base_url 從環(huán)境變量 API_BASE 讀取。 2. 每個(gè)用例必須有 assert 斷言覆蓋正常返回和至少一個(gè)異常入?yún)ⅰ?3. 只輸出 Python 代碼不要解釋不要 Markdown 代碼塊標(biāo)記。 接口描述 {{INTERFACE_DESC}}生成腳本gen_tests.py調(diào)用 TaoToken 的兼容接口import json import time import requests from pathlib import Path from settings_loader import load_settings def build_prompt(cfg, interface_desc): tpl Path(cfg[generation][prompt_template]).read_text(encodingutf-8) return tpl.replace({{INTERFACE_DESC}}, interface_desc) def call_llm(cfg, prompt): url cfg[llm][base_url].rstrip(/) /v1/chat/completions headers { Authorization: fBearer {cfg[llm][api_key]}, Content-Type: application/json, } payload { model: cfg[llm][model], messages: [{role: user, content: prompt}], temperature: cfg[llm][temperature], max_tokens: cfg[llm][max_tokens], } last_err None for attempt in range(cfg[llm][max_retries]): try: resp requests.post( url, headersheaders, jsonpayload, timeoutcfg[llm][timeout_seconds], ) resp.raise_for_status() return resp.json()[choices][0][message][content] except Exception as e: last_err e time.sleep(2 ** attempt) raise RuntimeError(f調(diào)用失敗: {last_err}) def main(): cfg load_settings() desc Path(interface_desc.txt).read_text(encodingutf-8) code call_llm(cfg, build_prompt(cfg, desc)) out Path(cfg[generation][output_dir]) out.mkdir(parentsTrue, exist_okTrue) target out / test_smoke_generated.py target.write_text(code, encodingutf-8) print(f已生成 {target}) if __name__ __main__: main()interface_desc.txt里寫(xiě)你要測(cè)的接口比如GET /api/v1/users/{id} 返回 JSON: {id: int, name: str, email: str} id 不存在時(shí)返回 404。跑一次生成export TAOTOKEN_API_KEYsk-你的key python gen_tests.py生成的tests/generated/test_smoke_generated.py大致長(zhǎng)這樣import os import requests BASE os.environ[API_BASE] def test_get_user_ok(): r requests.get(f{BASE}/api/v1/users/1, timeout5) assert r.status_code 200 body r.json() assert isinstance(body[id], int) assert isinstance(body[name], str) def test_get_user_not_found(): r requests.get(f{BASE}/api/v1/users/999999, timeout5) assert r.status_code 404執(zhí)行冒煙export API_BASEhttp://localhost:8000 pytest -q tests/generated/test_smoke_generated.py --junitxmlreports/smoke.xml成功時(shí)輸出類(lèi)似2 passed in 0.42s到這里從「接口描述」到「斷言通過(guò)」的閉環(huán)就跑通了。CI 里把gen_tests.py和pytest串成兩個(gè) step 即可生成失敗會(huì)因重試機(jī)制自動(dòng)兜底。5. 本篇常見(jiàn)錯(cuò)排查401 Unauthorized九成是 Key 沒(méi)注入。檢查 CI Secret 名是否和 settings.json 里的api_key_env一致本地確認(rèn)echo $TAOTOKEN_API_KEY有值。別把 Key 直接寫(xiě)進(jìn) settings.json。404 Not FoundBase URL 拼錯(cuò)。正確是https://taotoken.net/api腳本里再拼/v1/chat/completions。如果 SDK 自己會(huì)補(bǔ)/v1就別重復(fù)加否則變成/v1/v1/...。生成的代碼帶 Markdown 圍欄模型偶爾會(huì)輸出 python 包裹。提示詞里已經(jīng)要求「不要 Markdown 標(biāo)記」若仍出現(xiàn)在寫(xiě)入前加一層清洗def strip_fence(code: str) - str: lines code.strip().splitlines() if lines and lines[0].startswith(): lines lines[1:] if lines and lines[-1].startswith(): lines lines[:-1] return \n.join(lines)斷言太弱或沒(méi)有斷言把提示詞里的「必須有 assert」寫(xiě)得更具體比如「至少一個(gè)狀態(tài)碼斷言和一個(gè)字段類(lèi)型斷言」。temperature 再降到 0.1 也有幫助。超時(shí)CI 網(wǎng)絡(luò)慢時(shí)把timeout_seconds調(diào)到 90max_retries保持 3。重試間隔用指數(shù)退避別固定 1 秒猛打。生成文件覆蓋了手寫(xiě)用例output_dir單獨(dú)放tests/generated別和手寫(xiě)測(cè)試混在一起。overwrite設(shè) true 只影響生成目錄。6. 把閉環(huán)接進(jìn) CI 與后續(xù)動(dòng)作GitHub Actions 里兩個(gè) step 就夠- name: Generate tests env: TAOTOKEN_API_KEY: ${{ secrets.TAOTOKEN_API_KEY }} run: python gen_tests.py - name: Run smoke env: API_BASE: http://localhost:8000 run: pytest -q tests/generated/test_smoke_generated.py --junitxmlreports/smoke.xml想驗(yàn)證模型本身是否正??梢韵扔媚P蛯?duì)話(huà)頁(yè)面發(fā)一條消息確認(rèn)通道通https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite長(zhǎng)期在 CI 里跑生成任務(wù)Key 用量和并發(fā)會(huì)上去Coding Plan 更適合這種持續(xù)編碼場(chǎng)景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewriteKey 管理和輪換在控制臺(tái)完成新 Key 建好后只改 CI Secretsettings.json 不用動(dòng)https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接口路徑或鑒權(quán)格式有疑問(wèn)時(shí)翻接入文檔里面有各語(yǔ)言示例https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite實(shí)測(cè)下來(lái)這套骨架最省心的地方是配置和代碼分離換模型只改 settings.json 的model字段換供應(yīng)商只改base_url測(cè)試腳本一行不動(dòng)。生成目錄獨(dú)立也避免了模型輸出污染手寫(xiě)用例。