看板 Soft_Job 關於我們 聯絡資訊
這系列最早是 2012 年的連載,時間過去八年, 最近常在想,一樣是寫程式,一樣是會犯粗心的錯。 為什麼 coding 的速度也好,品質也好, 找問題的速度也好,不同人落差還是顯著。 當年寫這系列主要是要介紹開發測試的有趣之處。 有興趣的可以 /笑談軟體測試 翻舊文。 ====== 今天要來繼續往下寫,當年也好,今日也好。 最多人問我的是,到底要怎麼寫啊, 真的要 unit 全面涵蓋嗎? 要怎麼改 code 不用重寫測試,很多很多的問題。 很多人也很常問,碰到一堆老 code 到底是要怎辦。 裡面有些是 IDE 能幫忙的,有些是自己的程式碼組織模式問題。 我自己是不會太倚賴 IDE ,但也不至於不用 IDE , 總之是人要比機體凶的那一派。 測資,測試模式,案例思維,可測試性在前幾篇都講過, 這篇要講的兩個點,一個叫"狀態",一個叫"介面"。 在看歷史代碼時,大的原則是程式碼不可能重頭改完的, 所以問題是,你如何切割出真正的局部戰場,並根據當前需要進入修正。 ====== 什麼是狀態(state),基本上就標準測試環節的術語, mock 跟 stub 都某個程度上都在解決狀態的問題。 狀態意指著某個暫存一些變數或資料的載體,小從記憶體,大從 db / file / network 都算數 他的特色是,在你還沒存取他之前, 他的值都有可能產生變化,並會因此導致測試的變動, 而讓測試的價值產生變數。 (當然你跟我說檔案、cache ,在你的情境是寫死不會動的, 所以也是某種靜態的資料,呃我不反對,但今天講的是有機會動的情況。) 跟狀態成對的是所謂的副作用, 指的是執行一段行為之後, 原本的作為環境資訊的狀態被修改了。 像是你刪除一則貼文,就找不到他了,這時候你就無法再對他做編輯的行為了。 這種狀態變更就是一種副作用。 副作用一般來說是必然會發生的,但有幾個大雷要避免, 一個是一般避免直接對傳入的參數進行內容的變更, 真要變更還不如回傳一個新的結果。 因為一般無法透過函式的存取介面,看見這個副作用的存在。 God object 會惹人厭, 主要一方面除了要準備這個物件的成本極高, 另一方面就是往往存在極為複雜的副作用。 所以當我們前提是在看歷史代碼時, 首先要留意的就是函式的分法跟副作用。 我們無法掌握前人的撰寫風格或是程式技巧, 但小心的梳理副作用,讓他在自己理解的範圍。 必要時調整程式碼來加強理解, 是閱讀歷史代碼所必須掌握的技能。 另外狀態的分立也是一個歷史代碼的觀察點, 如果你發現同樣的狀態值,在多個 DTO 甚至 Entity 都存在, 那一般意味著這是個好用(參考價值高)的狀態, 但同時也要留意對這個狀態,各種計算是否有標準化, 如用 util 包裝過或寫成函式來操作。 以我現在的專案為例,人資系統除了真實時間以外, 往往都還有一個計薪日(又稱歸屬日)的概念。 所以多數資料在處理發生時間時都會加計歸屬日。 那如何計算真實時間跟歸屬日的關係, 就應該有個有效的函式能夠盡可能統一處理, 而不是每次都各顯神通重新組裝計算。 哪些要優先,哪些可以晚點再處理,在追蹤代碼時, 要有意識的去看哪些東西被引用的多,哪些東西被引用的少。 在心裡面對所有的 entity/dto ,建立危險性跟重要性的判斷。 然後,那些 common util 當然也是該優先涵蓋的區域。 另外很多人談狀態,談的是單一類別或兩層內的狀態。 但真正麻煩的 state 是要看整個 request life cycle 的 call stack , 特別是這年頭 closure / map & reduce 的寫法盛行。 很多參數雖然是 local variable ,實質上卻會傳遞的非常深入, 也往往會是很多操作轉介的核心資料。 上游資料變化對下游結果非常敏感, 這種時候如果沒仔細想清楚往往會有非預期的副作用出現。 如何適當的中斷這麼多層的 parameter 傳遞, 讓狀態減少到人腦可以思考跟負擔的層次,就是一個重要的問題。 我理想中的狀態比較像是遞回那種做法, 每一層都只負責自己需要知道的, 但有更外層會收集子函式的結果來做該做的結果。 (這裡也搭配 tell, dont ask 開發精神。) ====== 再來是 interface , 歷史代碼有時候難免會出現義大利麵。 這時候要搭配前面的概念,根據手上所允許的時間, 在目標區域自己要修或觀察的核心程式碼, 將其轉至為一個簡單的 input output 無狀態的映射。 這講起來很抽象,比方說算上下班打卡是不是該打卡這件事情好了。 這個問題總共有幾個變數, 一個是他有沒有設定要求打卡提醒的參數, 一個是他當天有沒有請假, 一個是他當天有沒有出差, 最後一個是他有沒有補登打卡的記錄。 在我這的實作這四個本來都是撈資料, 所以可能就是類似這樣: function 大執行函式(){ var 打卡異常提醒 = false; var 設定資料 = dao.讀db設定(); var 請假資料 = dao.讀db請假(); var 出差資料 = dao.讀db出差(); var 補登資料 = dao.讀db補登(); if (.....){ /*判斷A*/ } if (.....){ /*判斷B*/ } if (.....){ /*判斷C*/ } } 可能一大個函式就長這樣, 但如果你想把這段程式轉為可測試性, 很多人直覺會是要去讓 DB 傳回該傳回的值。 更有概念一點的可能會想去做 dao 的 mock , 我們這裡很好心的是假設是同一個 dao, 現實世界可能還跨 db context or service 。 但就我自己的習慣是會這樣改 function 大執行函式(){ var 設定資料 = dao.讀db設定(); var 請假資料 = dao.讀db請假(); var 出差資料 = dao.讀db出差(); var 補登資料 = dao.讀db補登(); return 檢查資料(設定資料,請假資料,出差資料,補登資料); } function 檢查資料(設定資料,請假資料,出差資料,補登資料){ if (.....){ /*判斷A*/ } if (.....){ /*判斷B*/ } if (.....){ /*判斷C*/ } } 主要的目的是把 db 的操作, 盡可能的在不浪費的前提下往前提, 並且將程式小心的區隔出無狀態跟有狀態的區域。 (有狀態的區域就是一種, 比單一類別再大一點的狀態容器。) 並且盡量把自己的目標區域, 設定成相對無狀態的模式,並加以測試涵蓋。 (邏輯相對穩定跟容易鎖定) ====== 有些人可能在這時候很快就會提出質疑, 一個是函式很肥很長不好切。 呃,這個問題的答案是,沒人叫你切那麼大, 正常情況下都是小規模堆疊, 如果堆幾次堆不出新的公約數來簡化查詢。 那就是複雜度本身就是複雜到需要這樣, 這時候只能跟著複雜度一起, 切出多區的狀態容器跟無狀態處理機。 另一個問題是,即使我們可以保護好無狀態的處理機, (特別留意,這裡的保護好,同時包括掌握住所有可能的副作用。) 可能有些人習慣還是看最後的結果是 yes 還 no , 會來質疑說,如果這四個資料庫查詢有一個錯怎麼辦。 概念上還是得寫四個資料庫,查詢跟檢查的排列組合, 如果你這麼想,那你這裡就是進入了思維誤區。 我只需要分開再寫四組 test, 涵蓋確保這四個 db 查詢如預期就好, 雖然這裡是帶狀態的,test case 不好寫, 但不好寫不代表做不到。 其次,我只要資料準備正確, 就能保證後面的無狀態處理機在[已知的案例], 前提下的行為都是穩定可靠的。 這點對於我們抽離狀態帶來的不確定性, 已經產生了很大的幫助。 另外,這種介面抽換的技巧,在重構時也非常有用。 在歷史代碼複雜且手上規格參考價值不高的時候, 如果是為了解決邏輯層的計算問題。 我會先切割出小型的區域,然後複製一份舊的無狀態函式版本保留作為對照組,然後將無狀態函式重寫。 使用舊的資料提供者並作 snapshot , 將這些資料騰到測試案例,接上新的計算器, 確保新舊兩個測試案例結果一致。 (若不一致不用驚慌,拿出規格跟資料, 找 user 確定誰是對的, 就我經驗,重寫很有可能找出陳年或前人逃避的問題。) 如果是效能問題,我通常是會重寫資料準備區域,並且用同樣的計算結果作為依據。 當然有時候會面臨要改善必須兩邊一起動的情況, 這種選擇我會放在最後面選, 同時因為能夠對照的路牌比前兩者少, 我會安排更多時間跟案例做測試驗證。 不管是多少的程式碼, 解決問題都只能從最小的單位一路向上堆疊。 很多人談到歷史程式碼往往視為是負債, 這篇介紹的做法,本質上是在讓你有機會藉由前人代碼撐竿跳。 達到同時確認品質跟思路,也不讓自己暴露在太大風險之中。 具體在執行的時候都非常簡單, 但心法卻不容易掌握跟理解。 有興趣可以從這角度想想看。 ----- Sent from JPTT on my Google Pixel 3 XL. -- 之間的世界,反抗軍啟蒙軍的交集 帶著 Android 去旅行、去發現 在身邊渾然不覺的 另一個世界。 全世界,都是我們的 足跡與遊樂場。 ~ The world around you is not what it seems. ~ http://ingress.tw -- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 61.231.28.42 (臺灣) ※ 文章網址: https://www.ptt.cc/bbs/Soft_Job/M.1597938487.A.566.html ※ 編輯: TonyQ (61.231.28.42 臺灣), 08/20/2020 23:50:59
aa08666: 推一個在看 08/20 23:56
vi000246: 先推再看 08/20 23:59
black209: 好文推 08/21 00:32
APTON: 最近一個人默默寫測試有感,心法真的要自己下去做才能慢慢 08/21 01:15
APTON: 體會 08/21 01:15
anandydy529: 推 08/21 01:56
magus: 推 08/21 02:42
magus: 按錯orz待會補推 08/21 02:43
magus: 補推啦 08/21 02:46
smart0eddie: 推 08/21 02:49
atpx: 感謝分享, 這些千錘百鍊的內功真的要有一定經歷才能體會 08/21 03:21
leo770429: 推 08/21 06:46
這系列其他網址參考 笑談軟體測試的幾個階段(一)測試緣起 https://www.ptt.cc/bbs/Soft_Job/M.1332567899.A.C00.html [閒聊] 笑談軟體測試的幾個階段(二) 測試路徑 https://www.ptt.cc/bbs/Soft_Job/M.1332601969.A.342.html [閒聊] 笑談軟體測試的幾個階段(三) 可測試性 https://www.ptt.cc/bbs/Soft_Job/M.1332681709.A.EB4.html [閒聊] 笑談軟體測試的幾個階段(四) 累積測資 https://www.ptt.cc/bbs/Soft_Job/M.1332697283.A.630.html [閒聊] 笑談軟體測試的幾個階段(五) 測試權重 https://www.ptt.cc/bbs/Soft_Job/M.1332863702.A.076.html ※ 編輯: TonyQ (223.140.28.208 臺灣), 08/21/2020 07:58:13
popmentos: 感謝 08/21 09:21
jixiang: 推 08/21 09:58
ian90911: 感謝分享 08/21 11:16
liukuai: 推,感謝分享 08/21 11:48
bigshawn: 推 08/21 12:42
zxcv860513: 推 08/21 12:51
jj0321: push 08/21 13:21
bnd0327: 推推 08/21 14:17
Diawchen: 推 08/21 14:56
milkstrong: 推 08/21 17:00
kyukyu: 推 08/21 17:33
onegoman: 推 08/21 21:52
jl40: 測試真的是無底洞 沒測試重構很有壓力 08/21 23:56
es8603: 推 08/22 08:13
TAKADO: 推 08/23 08:50
mybluesky: 推,感謝分享 09/05 18:29