Re: [案例] Unit Test
我跟我的廠商說
來導入TDD在這個專案吧
並且跟他們說
一邊寫SPEC 一邊就可以寫測試案例啦
他們總是跟我回應這不適合這個專案
對於她們來說測試是最後寫的
並且總是可以找到理由證明測試是不切實際的
我才發現書上寫的還真有道理
(我忘記是重構還是敏捷還是clean code了)
當我展示我的web service測試案例的時候
並且展示如何用TDD去修正專案MVC及DAO架構的時候
我才發現環境建立的重要性
對於測試,我認為有
1.針對需求建立的測試,主要是SPEC
2.檢測系統元件架構的模組化程度,如果有某元件無法測試,
就表示架構有問題,而非測試不適合(哀~這就是廠商跟我說的不適合阿)
因此根據著眼點的不同,要建立的環境也不太一樣
以我的web service為例
測試的範圍是該服務的全部,並且要是黑盒子測試
因此每個測試基本上只有三行(其實不止拉)
1.建立資料
2.執行某服務
3.檢查結果
public void test_helloworld(){
Data data = loadHelloworldData();
webservice.hellowWorld(data);
Assert.assertEquals("Hello World",data.getRespose());
}
接著根據你的需求分別定義
例如我的測試資料都是以資料庫為導向
所以我就是
public void test_helloworld(){
String id = "helloworld";
loadSQL(id+".sql");
Data data = instanceData(id);
webservice.hellowWorld(data);
Assert.assertEquals("Hello World"
,queryforOne("select message from xxx where id=?",id));
}
通常SQL也要抽離,但這是我的習慣啦 XD
因為測試有的時候是很赤裸裸的啦
因為測試環境的不良
往往導致測試案例不好撰寫
1.例如測試資料要如何建立呢?
2.而且有些元件竟然很難測試
在此我的感想就是
1.建立良好的環境供測試
2.如果你無法測試某個元件,一定是你的元件有問題,而非不適合測試
所以可以倒過來說,如果對於某物件架構感到疑惑,那麼為該架構寫測試案例
如果發現沒辦法好好寫,那就是架構有問題,
同時也可以從寫測試案例中,獲得架構的靈感喔
因此測試並非是在專案開發好才做,是可以先做或是一邊做都非常適合喔
所以大家一起來TDD吧
※ 引述《qrtt1 (有些事,有時候。。。)》之銘言:
: 此文是討論這篇提問的案例:
: ┌─────────────────────────────────────┐
: │ 文章代碼(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: 163.29.29.131
→
06/13 18:00, , 1F
06/13 18:00, 1F
討論串 (同標題文章)