[案例] Unit Test

看板java作者 (有些事,有時候。。。)時間11年前 (2013/06/09 17:01), 編輯推噓1(100)
留言1則, 1人參與, 最新討論串1/2 (看更多)
此文是討論這篇提問的案例: ┌─────────────────────────────────────┐ │ 文章代碼(AID): #1Hj2uDh_ (java) [ptt.cc] [問題] 請問關於Junit Test Code? │ │ 文章網址: http://www.ptt.cc/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)

06/09 17:22, , 1F
非常感謝你Orz
06/09 17:22, 1F
文章代碼(AID): #1Hj4HbRl (java)
討論串 (同標題文章)
以下文章回應了本文
完整討論串 (本文為第 1 之 2 篇):
案例
1
1
文章代碼(AID): #1Hj4HbRl (java)