java 板


LINE

此文是討論這篇提問的案例: ┌─────────────────────────────────────┐ │ 文章代碼(AID): #1Hj2uDh_ (java) [ptt.cc] [問題] 請問關於Junit Test Code? │ │ 文章網址: http://webptt.com/m.aspx?n=bbs/java/M.1370762765.A.AFF.html │ │ 這一篇文章值 229 Ptt幣 │ └─────────────────────────────────────┘ 由於上一篇的 Unit Test 實在長得不好看, 加上它被鎖文了就不針對他的問題去回應。 另外程式碼長得不好看,看了只會想譙人。 回到那個問題的本質是或一組管理學生分數的程式, 它的測試重點在於分數經過修改後仍然是符合邏輯的。 至於符合邏輯的細節是什麼? 1. 符合 spec 2. 符合常理(雖然這點偶爾跟 1. 衝突) 我說那個 code 很醜是因為測試程試還要去搞定 user input 的東西, 要去弄 System.in System.out,有太多跟 business logic 本身無關的東西。 由於沒看過完整的需求,由原先的 test case 試著還原設計。 以下是我的設想。 /* GradeSystem 長成這個樣子,可以幫你找出某個 ID 的分數 */ public class GradeSystem { Map<String, Grade> grades = new HashMap<String, Grade>(); public Grade findGradeByID(String userId) { Grade grade = grades.get(userId); if (grade == null) { throw new NoSuchIDExceptions(); } return grade; } public void setGrade(String userId, Grade grade) { grades.put(userId, grade); } } /* Grade 代表分數,它同時知道它代表哪一個 ID 與名稱 */ public class Grade { String id; String name; Vector<GradeItem> items = new Vector<GradeItem>(); public void setId(String id) { this.id = id; } public void setName(String name) { this.name = name; } public void newItem(String name, int score) { GradeItem item = new GradeItem(); item.setName(name); item.setScore(score); items.add(item); } public GradeItem getItem(String name) { for (GradeItem item : items) { if (item.getName() != null && item.getName().equals(name)) { return item; } } return null; } public int sum() { int sum = 0; for (GradeItem item : items) { sum += item.getScore(); } return sum; } } /* 由於要計分幾次未定,在 Grade 內另外用 GradeItem 記錄實際的分數 */ public class GradeItem { String name; int score; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getScore() { return score; } public void setScore(int score) { this.score = score; } } /* 一個基本的測程式,它目前只完成了初始資料設定 */ public class GradeSystemTest { private static final String USER_NAME = "凌宗廷"; private static final String USER_ID = "962001044"; Random random = new Random(); GradeSystem gradeSystem; String[] gradeItmeNames = { "Lab1", "Lab2", "Lab3", "Mid-Term", "Final exam" }; int totalScore = 0; @Before public void setUp() { gradeSystem = new GradeSystem(); prepareTestData(); } private void prepareTestData() { Grade grade = new Grade(); grade.setId(USER_ID); grade.setName(USER_NAME); for (String itemName : gradeItmeNames) { int score = anyScore(); totalScore += score; grade.newItem(itemName, score); } assertEquals(grade.sum(), totalScore); gradeSystem.setGrade(USER_ID, grade); } @Test public void testChangeScore() { /* 在這裡我們要測試程式邏輯對不對 */ } protected int anyScore() { int score = random.nextInt(200); if (score >= 0 && score <= 100) { return score; } return anyScore(); } } ====================================================================== 關於測試資料,儘可能使用隨機產生的。 除了一些邊界條件的 CASE 都用隨機的! 所以在初始分數的設定,我們用隨機的分數,只要它是 0~100 就行了。 更理想是連 item names 都隨機(產生出 1 筆以上的 item) private void prepareTestData() { Grade grade = new Grade(); grade.setId(USER_ID); grade.setName(USER_NAME); for (String itemName : gradeItmeNames) { int score = anyScore(); totalScore += score; grade.newItem(itemName, score); } assertEquals(grade.sum(), totalScore); gradeSystem.setGrade(USER_ID, grade); } 在準備資料的過程,我們已經有第 1 個驗證: assertEquals(grade.sum(), totalScore); 測試分數有沒有改好的整套邏輯是,各別分數要對,總分也要對。 因此我們驗證它的時候,不是只有驗證單一項目的分數 還有綜合起來的結果是不是對的,更符合實際的情況應該要有權重配分。 那麼回到主要的「改變分數」的邏輯驗證: @Test public void testChangeScore() { } 要改變分數我們必需要: 1. 找到改變的項目 2. 重新設定該項目的分數 改變完成後,我們需 3. 取出該項目的分數是否符合預期 @Test public void testChangeScore() { Grade grade = gradeSystem.findGradeByID(USER_ID); for (String itemName : gradeItmeNames) { GradeItem item = grade.getItem(itemName); // 1. int oldScore = item.getScore(); int newScore = anyScore(); item.setScore(newScore); // 2. assertEquals(item.getScore(), newScore); // 3. } } 單項的驗證雖然做了,但它仍是不足的。 做 Unit Test 不是讓你單純測試你的 getter & setter 有沒有正常運作 改變單一項變的分數會影響最終的結果,這個結果的改變需要符合期待 這才是 Unit Test 要做的工作: 「驗證物件隨著狀態改變後,行為仍符合預期」 那我們該多期待什麼?對一個算分系統來說,至少你改單項總分要對。 (電視台那個選票系統常常教壞別人,常弄得單項加起來不是總分XD) 那麼總分該怎麼測試它呢?在你設定新的分數前,你可以用新的分數預測出總分 所以我們會這麼寫: @Test public void testChangeScore() { Grade grade = gradeSystem.findGradeByID(USER_ID); for (String itemName : gradeItmeNames) { GradeItem item = grade.getItem(itemName); int oldScore = item.getScore(); int newScore = anyScore(); item.setScore(newScore); assertEquals(item.getScore(), newScore); int newTotal = totalScore + (newScore - oldScore); assertEquals(grade.sum(), newTotal); totalScore = newTotal; } } 這樣子的設計與驗證不是簡單又易懂嗎? 要如何證名簡單又易懂?(思)... 大概只能說,我註解比你少,但多數人應該覺得我的 code 比你好看!? --



※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 114.43.116.109 ※ 編輯: qrtt1 來自: 114.43.116.109 (06/09 17:06)
1F:推 pugboy:非常感謝你Orz 06/09 17:22







like.gif 您可能會有興趣的文章
icon.png[問題/行為] 貓晚上進房間會不會有憋尿問題
icon.pngRe: [閒聊] 選了錯誤的女孩成為魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一張
icon.png[心得] EMS高領長版毛衣.墨小樓MC1002
icon.png[分享] 丹龍隔熱紙GE55+33+22
icon.png[問題] 清洗洗衣機
icon.png[尋物] 窗台下的空間
icon.png[閒聊] 双極の女神1 木魔爵
icon.png[售車] 新竹 1997 march 1297cc 白色 四門
icon.png[討論] 能從照片感受到攝影者心情嗎
icon.png[狂賀] 賀賀賀賀 賀!島村卯月!總選舉NO.1
icon.png[難過] 羨慕白皮膚的女生
icon.png閱讀文章
icon.png[黑特]
icon.png[問題] SBK S1安裝於安全帽位置
icon.png[分享] 舊woo100絕版開箱!!
icon.pngRe: [無言] 關於小包衛生紙
icon.png[開箱] E5-2683V3 RX480Strix 快睿C1 簡單測試
icon.png[心得] 蒼の海賊龍 地獄 執行者16PT
icon.png[售車] 1999年Virage iO 1.8EXi
icon.png[心得] 挑戰33 LV10 獅子座pt solo
icon.png[閒聊] 手把手教你不被桶之新手主購教學
icon.png[分享] Civic Type R 量產版官方照無預警流出
icon.png[售車] Golf 4 2.0 銀色 自排
icon.png[出售] Graco提籃汽座(有底座)2000元誠可議
icon.png[問題] 請問補牙材質掉了還能再補嗎?(台中半年內
icon.png[問題] 44th 單曲 生寫竟然都給重複的啊啊!
icon.png[心得] 華南紅卡/icash 核卡
icon.png[問題] 拔牙矯正這樣正常嗎
icon.png[贈送] 老莫高業 初業 102年版
icon.png[情報] 三大行動支付 本季掀戰火
icon.png[寶寶] 博客來Amos水蠟筆5/1特價五折
icon.pngRe: [心得] 新鮮人一些面試分享
icon.png[心得] 蒼の海賊龍 地獄 麒麟25PT
icon.pngRe: [閒聊] (君の名は。雷慎入) 君名二創漫畫翻譯
icon.pngRe: [閒聊] OGN中場影片:失蹤人口局 (英文字幕)
icon.png[問題] 台灣大哥大4G訊號差
icon.png[出售] [全國]全新千尋侘草LED燈, 水草

請輸入看板名稱,例如:WOW站內搜尋

TOP