Re: [討論] 工作上寫單元測試的比例
因為大家的討論都很基於心法
實作上相對很模糊
利用這個機會也跟大家請教實作上的方式
因為最近工作被指派要針對公司產品的程式做整理,其實運作都還好
只是大家功能是一層疊一層,一堆巢狀邏輯,跟依賴中的依賴
也沒有任何的測試跟註解,當然也沒有任何測試
舉個例子
有個功能是儲值,可以接受銀行、信用卡、或是一個臨時帳號
然後儲值前需要身分驗證,然後銀行的話要檢查是不是有綁定
總之每個方式都有一些共用,或是非共用的行為
目前的程式像這樣
func 儲值(方式){
switch 方式{
case 方式1:
if 符合條件1 {
if 符合條件2 {
if 呼叫api-1 成功{
更新介面1
}
}
}
case 方式2:
要符合不同的巢狀條件,然後呼叫另一隻api,一樣根據結果更新不同的介面
case 方式3:
又是不同的條件跟api
}
}
像這樣的程式,不知道測試怎麼寫?
單看這個函式的cyclomatic complexity,大概是20幾吧
如果把測試寫成涵蓋所有的可能
像是=>
當選擇方式1儲值,且不符合條件1,就要如何如何
整個測試就寫不完了
而且條件本身又依賴於其他的api或是系統參數
就算寫完了測試,涵蓋率雖然高,但我覺得是沒有解決核心的問題
我就只是要確認我程式本身的邏輯沒問題,不需要涵蓋別人的東西
這邊我實在很好奇大家的作法是怎樣?
是跳過不做,還是真的就寫一個涵蓋所有可能的測試
我的選擇是先重構啦
先定義一個抽象的RechargeCommand介面
包含執行儲值操作所需的方法
其中execute()會統一回傳一個Result物件,代表儲值結果
接下來為每種儲值方式實作具體的命令
例如BankRechargeCommand、CreditCardRechargeCommand和AutoRechargeCommand等
然後一些通用的行為,像是登入驗證、密碼輸入等,用裝飾者模式
定義一個RechargeCommandDecorator抽象類別,繼承自RechargeCommand介面
最後為每種需要共用的行為創建一個具體的裝飾者,繼承自RechargeCommandDecorator
例如
LoginCheckDecorator和BioAuthDecorator
呼叫的時候,根據需要去組合不同的儲值方式,跟需要的共同行為
回傳值也都是相同的Result物件
呼叫會像這樣(我是ios swift)
let bankRechargeCommand: RechargeCommand = BankRechargeCommand()
let loginCheckDecorator = LoginCheckDecorator(bankRechargeCommand)
let bioAuthDecorator = BioAuthDecorator(loginCheckDecorator)
let result = bioAuthDecorator.execute()
呼叫api的部分用策略模式做依賴注入
在command實作中,接收一個RechargeAPIService,預設是真實呼叫
做單元測試的時候就單獨傳入mock service
測試也改成針對不同的儲值方式做測試
而不是去涵蓋所有的可能路徑
這樣就可以大幅減少測試案例的撰寫,也可以節省複製貼上的時間
我只要確定我的內部邏輯正確,理論上不管怎樣的路徑應該都不會出錯
也不會被綁定在外部依賴
整個流程大概就是把一個巢狀判斷函式
更改為 command+decorator 模式,根據需要呼叫與包裝對應的行為
然後外部的依賴透過注入,在測試時使用mock避免被干擾
也請大家看一下
不知道這樣的修改是不是可以
還是應該以涵蓋所有的可能狀況為優先 @@?
※ 引述《TonyQ (得理饒人)》之銘言:
: 先說我不是故意要回兩篇,
: 但剛看到 landlord (就 joey chen, 江湖名 91) 在 FB 的回應,我覺得也蠻好的,
: 他說他最近在忙沒空過來,我問過他之後幫他轉過來。
: 以下基本上逐字照轉
: (source from https://tinyurl.com/rxyerfyw )
: 其實講到底根本原因反而是跟產品程式碼的設計能力有關,
: 產品程式設計得越好,測試程式越容易寫,越好測。
: 真正需要在測試中做假模擬(隔離)的部分,
: 屬於外部(擁有權不在我們手上的部分),
: 例如外部系統的服務(走通訊協定出去,且不屬於我們可以維護跟上版的服務)、
: 三方(package/SDK)。而 DB, redis之類的 cache 甚至是不需要特別被隔離開的。
: 這是由於現代科技的便利,讓我們有機會把越來越接近端到端測試的一類,
: 比例逐步拉高的可行性比過去容易得多。
: 另一個重點則是當設計越來越偏向高內聚,simple design,
: 把 code smell 消除到最後回很自然地提煉出 domain model,
: 有了 domain model,
: 最複雜的 domain logic 處理一堆散落資料的邏輯都被內聚到model裡面,
: 沒有 application 層的依賴,model 的單元測試也很好寫。
: 結論:
: 1. 要有能力在 legacy 上重構出可測試性
: 2. 要有能力做出穩定的端到端測試
: 3. 要能精煉設計,將散落的資料內聚在一起
: 來代表 domain 的概念提供 domain 的行為,
: 因為設計上本來就沒有外層的依賴,model的方法也都精簡短小,甚至鮮少回傳值,
: 自然 API 易用性跟測試都可以比過去萬惡的三層式架構+內嵌無限層依賴注入
: 的手風琴架構來得簡單跟好測許多。
: 現在大部分的依賴(注入)都不是本質上需要的,而是被開發人員硬生生切得支離破碎的。
: 補充一下 TonyQ 內文最後一點:
: 「如果都沒被報 bug,你也沒有修改它的需要時,幫它加測試幹嘛?」
: 這超級重要的,這種情況下加測試往往適得其反,
: 只會建立偽陽性/陰性的測試結果,勞民傷財還造成干擾。
: ※ 引述《TonyQ (得理饒人)》之銘言:
: : 底下這是比較「野性」」的作法,算是實務專案的經驗:
: : 其實我覺得你到一個完全沒有測試的專案,要分兩個策略:
: : 1. 補重要主線的 integration test 反正哪邊常被報修就補哪邊。
: : 如果一開始補不上去就先做下一點,理論上常被報修的地方會一直出現在下一點,: : 累積多了就可以變成1了。
: : 2. 假設自己是維護歷史功能,提昇自己維護部分的可測試性。
: : 舉實際案例好了,假設我今天再做一個算金流手續費的專案,
: : 發現過去算手續費假設有11個地方寫了11次好了,所謂的高耦合不外乎如此。
: : 我會先寫個 util 把輸入跟輸出「去狀態化」,然後針對這個 util 寫,
: : 然後這個 util 的單位以「去狀態化」成本可控,可在手邊開發時間允許的範圍進行。
: : 白話文:我橫豎都得手動測試,那就把手動測試的部分,
: : 盡可能的透過 test code 來進行。
: : 如果不想接著維護的話也很單純,任務結束後把 test code comment 掉或移除就行。
: : 題外話,11個地方,我會選擇先取代一個地方,
: : 然後等其他10個地方有需求變更時,一個個整併,補強測試條件。
: : 很多人會說,那為什麼不一次改11個,理由是你的開發時間跟成本允不允許。
: : 更重要的是你的QA資源夠不夠,因為寫正常的Test最累的是準備測試情境,
: : 這真的是會花掉比寫test更多的時間。
: : 如果列不出完整場景,貿然修改既有的code只是在裸奔。
: : 有需求的部分是被迫裸奔,但沒需求的部分不用主動當暴露狂,
: : 等待驗證過再慢慢統一。
: : 大原則就是:你橫豎都是要測試的,只是手測還是寫程式測,除了跟 gui 有關的東西,
: : 多數的情況下寫程式測試都還是比較省時間的。
: : 更棒的地方是,在這種策略下,你往往可以用比同事少30% 的時間完成任務,
: : 而且因為你的測試成本比較低,所以品質也會比較好,出問題的時候也更容易對焦。
: : 然後我通常是會跟同事說我寫了幾個 test case,
: : 他們願意看就看,不願意看我就放著。不會勉強他們加入。
: : 如果你做不到可以用比不寫測試更短的時間來完成任務,
: : 那你學的測試根本性的就有問題,不寫也罷。XD
: : 3. 極端情況: 如果都沒被報bug,需求也都很小?
: : 那你操心他幹嘛 XD
--
剛好之前寫過類似的題目,有帶到重構的過程跟影片
當時也有朋友支援各語言版本庫,給大家參考一下
當然,寫測試的話還是得知道有哪些情境,才能用測試描述
但這個重構的過程,即使沒測試,有pair跟 code review
也不會有太大的成本跟風險問題
神...Orz
你的最後一段假設就是在說如果單元測試都沒問題整合測試
跟上線理論上都不會爆掉
實際上呢?如果這個理論正確那就不用寫整合測試了
所以要保證不爆掉當然需要所有的輸入變數的可能性跟路徑
都測試過,符合預期,才有可能保證上線不爆掉
其實因為我的是app,所以其實我這段測試其實更偏向整合測試而不是單元測試 只是也很好奇 所謂的整合測試,在牽涉到外部的時候,需要一起測嗎? 因為我的測試程式本身就包含了登入檢查、生物辨識、呼叫api 只是這些部份我使用的是mock 這些物件的測試應該用其他的方式或包裝,不應該影響儲值這件事跟儲值的測試 加上我回傳的是一個通用的Result物件 這個Result要不是成功(包含成功儲值的相關資訊),要不是失敗(失敗的相關資訊) 流程中如果任何一個環節失敗了,對呼叫端來說都是收到失敗的Result 這時候就是處理失敗的介面與邏輯 原則上我還是會在測試中補上測失敗的測試案例 但應該就是幾個常見的,不會去涵蓋每一個 我的目標還是很簡單,我要確認的是程式這邊的邏輯有沒有問題 輸入變數已經被command模式限制了,以這個案例來說,好像不知道還有啥變數 且不管哪個環節錯,他就是拋Result<Error> 額外的測試,說真的,我覺得自己按一下比較快 理論上也不應該出現沒捕捉到的錯誤 真的有...大概也是很稀少的案例,這個就交給內部測試或是實際使用者吧 冏rz
※ 編輯: langrisser19 (59.120.34.91 臺灣), 05/02/2024 15:59:22"如果任何一個環節失敗了,對呼叫端來說都是收到失敗的"
這個可能 app 比較沒感覺,但是鑑別不同的失敗對debug重要
題外話你這個情境算是很特定的情境了,我自己在這個時候會
思考的問題是,「假設我的程式出錯了,我能不能避開最大的
傷害。」 這也是一個可以考慮的事情。
理論上牽涉到第三方服務的時候你要mock第三方服務
實務上第三方有提供測試區的話我會直接開一個測試區db直
接在上面測試
寫一個新的加測試
巢狀太多了,重構看看能不能guard clauses
太巢惹, ealry return 用一下該測的東西分一下你自然知
道 test 要怎麼寫
當你發現UT寫不下去時,你要的是重構
41
首Po想請問一下 大家工作上寫單元測試的情況 1.大部分寫完一個功能, 就馬上完成單元測試 2.先把該做的功能寫完, 再回來統一寫單元測試 3.不怎麼寫單元測試5
你講的三種我都做過,還有第四種:TDD 1. TAD,講白了就是先射箭再畫靶,如果你箭射對了那當然沒問題 但如果你一開始就射錯了還忘記拔出來,就是無效的測試 2. 同樣也是TAD,這個是我們被要求做的,code不是我們寫的、但我 們要幫其他team補測試。我主管也覺得很奇怪、我也覺得很奇怪,3
我覺得命題不是寫/不寫。 真的該問的是,工作上 reviewer 也好、或者是開發流程也好, 有沒有辦法【正常的判斷】 test 是不是寫對的。 XD 起碼是 team 能有一定認同的前提。 其實最後都回到 quality check,不然只是繞圈圈而已。 XD17
我想補一個情境 當到新公司或轉到新單位時 發現沒有在做unit test 此時身經百戰寫過上千次unit test的你 會選擇憑一己之力6
底下這是比較「野性」」的作法,算是實務專案的經驗: 其實我覺得你到一個完全沒有測試的專案,要分兩個策略: 1. 補重要主線的 integration test 反正哪邊常被報修就補哪邊。 如果一開始補不上去就先做下一點,理論上常被報修的地方會一直出現在下一點, 累積多了就可以變成1了。1
先說我不是故意要回兩篇, 但剛看到 landlord (就 joey chen, 江湖名 91) 在 FB 的回應,我覺得也蠻好的, 他說他最近在忙沒空過來,我問過他之後幫他轉過來。 以下基本上逐字照轉 (source from )7
ㄅ是啊,你應該是先有需求才有測試, 通常是先假設已經在線上的已經經過線上考驗。 如果沒有這種需求,你根本就不應該整理。 我個人認為任何在沒有需求的前提下情況下整理程式碼, 是一個浪費自己時間又沒意義的行為。2
原則上要寫測試的話我會用很古老的 TDD 的方式做,先寫測試之後再寫實作。 現在的話則是寫完測試之後 Copilot 就幫我寫完一半了,然後就開始 review copilot 的 code 了。 目前經驗上能不能寫測試的話我認為有三個維度會是主要影響關鍵,提供參考: 1. 文件是否齊全16
分享最近遇到的鬼故事 當初開發完A功能後有順手寫了UT確保該功能基本能動 後來有同事在開發B功能時把他的B功能加進去我的UT default flow內 也沒有請我code review 導致我在跑UT時發現不預期的行為
33
Re: [討論] Unit test 的撰寫請益先說結論,先都不要寫。 Legacy system 要先補大範圍的 integration test,確定整體的行為是對的。 如果 code 沒有要再改,不用補細部 unit tests。 原因是因為,原本 API 可能因為設計不良,導致無法寫 unit test 得先 refactor 才有辦法讓它變成 testable,這情況就要先 refactor 再補 UT23
Re: [討論] 怎樣算是一個合格的junior cpp programme針對關於 TDD 的討論另外回一篇好了 覺得用推文太長了 XD : 推 stupidlove0: 朝聖!重要的真的是unit test 08/23 18:47 : → HZYSoft: 回樓上 TDD 問題,TDD 不只要測試,還要先寫測試才寫code 08/23 21:33 : → HZYSoft: 很多人無法習慣這種順序,是否一定要 TDD 這有爭議 08/23 21:3420
Re: [請益] Spring boot的依賴注入降低耦合的例子在這個時代依賴注入最重要的用途,特別是在後端開發是讓Application 在多個不同的 環境下(Development, Production, local, etc) 能夠根據profile 組出能正確執行的Application 多型在這裡當然有他的地位,但是一般來說,大部分不接觸system boundary的service objects 是不太需要多型的,如果是java,那種一個interface 只有一個implementation17
Re: [請益] Spring boot的依賴注入降低耦合的例子先講結論: DI(Dependency Injection) 跟 IoC(Inversion of Control) 的原始目的都不是解耦 接著說明一下 DI 跟 IoC: 首先 IoC 的目的是控制權的轉移,如原 PO 文章裡面原本是在 Employee 裡面 new Address,而採用 IoC 之後這樣的行為就轉移到外部來決定16
[討論] Unit test 的撰寫請益先說我對 Unit test 的看法:測試單元(可能是 function)的邏輯是否正確 好,進入正題 小弟最近剛工作,稍微讀了一下負責的 project 的程式碼後, 要開始開發 Unit test。 現況是,各個 file (.c) dependency 很重,13
Re: [討論] Unit test 的撰寫請益先說在前面 雖然聽起來很幹話,但很多東西沒有標準答案 有時是合適度的問題,也可能是喜好(品味?)的問題 同一個題目,實際的 code 長得不一樣,可能也會用不同的方法處理 另外,除了資源豐富到人力充沛到不行的專案,以及幾乎沒有時程壓力10
Re: [請益] Spring boot的依賴注入降低耦合的例子很久沒寫Java了 就個人觀念提供簡單思考線索 基本上根據你的內容覺得你對解耦合還沒有很理解 講直白一點 當你import類別就是耦合了3
[請益] 如何實現單元測試多於整合測試?將單元測試實作於專案時,發現絕大部分API都是針對資料庫做CRUD,這部分程式透過in memory 寫了整合測試,越寫越覺得不對勁,心想單元測試數量不是應該要最多? 網路文 章、影片或實體書籍大多也在探討如何寫單元測試,整合測試資源相對少,在想是不是我 哪裡做錯了,懇請各位大神指教。 --- 先確認一下,不知道你們是不是用一些潮潮 der 框架, 然後那框架的官方文件給的範例是看起來簡潔漂亮清楚的一兩行 code, (例如 service 裡一個查詢 > return 結果) 然後你們把範例 copy 過來改,直接往裡面塞邏輯? 如果是這樣,可能需要先做的是把 CRUD 分割出去,