綠燈 | 紅燈

妙思資訊

2021年7月18日 星期日

台灣-景氣對策信號

景氣燈號

恐慌指數


初領失業救濟金人數

失業低於5週的短期失業人數

零售銷售年增率,個人耐久財消費支出年增率

耐久財訂單

  • 私人固定資本投資年增率

  • 製造業耐久財新訂單

進出口年增率



 

2012年6月1日 星期五

Google PageRank 的迷失

什麼是Google PageRank (PR)?就是Google 用來衡量網站活躍度的指標。指標數值從0~10;數值越高,人氣就越旺。那要如何知道該網站的 PageRank 是多少呢?如果瀏覽器有裝Google Tool Bar 就可看到PageRank 的數值,如圖所示。





如果連結到你網站的連結數越多是否Page Rank 就越高呢?那可不一定,如果對方網站的
PageRank 很高,只要一個連結,就可抵上數十個或是上百個低PageRank 網站的連結。
以下圖表為各級PageRank 間的連結的參考表。


舉例來說, PageRank=4 (PR4)的網站,需要3055條PR1的連結,或只需要101條PR3的連結,所以不是連結數越多越好,而是品質好的連結越多越好。
那PR6 高的網站,所給的連結是不是就是品質好的連結呢? 那可不一定,如果這個網站只放一條連結到你的網站,那這條連結決對是高品質的連結;但是如果這個網站往外連結的網站有上百個,那PageRank就會被這上百條連結所稀釋,這時你要到的連結可能連PR2都不如。有些PR高的網站,你只要給錢就讓你放連結,但這個連結的PR因為被稀釋了,所以不會是你想像的那麼高。
最後,為何同性質的網站在查詢關鍵字時,有時PR高的網站查詢結果在後面,反而是PR低的網站查詢結果在前面呢?原因是,除了連結的品質外,還要考慮連結的類別。
如果你的網站是跟美容有關,最好連結到你的網站也跟美容的主題有關,如果連結到你的網站都跟程式有關,若每條連結的PR都差不多,但因類別是跟美容的比較符合,相對的搜尋排名也就會在前面。

所以PageRank 高並不是決對必勝,不要過度迷失了方向!


2012年5月30日 星期三

先修Bug 還是先寫測試

今天經過六褔客棧,發現後面有一尊四面佛廟,香火鼎盛,這尊佛像有4個面,不同本地的佛像只有一個面向。這就讓我想起測試時,我們只會測某一種面向的情節,而忽略其它面向,當某種情況下,這個面向被製造出來後,程式的Bug也就產生了。
當程式遇到bug,一般反應就是先把bug找出,修復它,就上線。但下次換人改code時,同樣的bug是否又會生出呢?通常又改出相冋bug的機會是很高的。
在TDD的方法論中有提到,當遇到bug 時,是先寫一支測試,測試在這種不同的面向情境是否會變成紅燈,如果在其它面向情境是綠燈,而在這個面向會產生紅燈,那這支測試就可抓到這個bug 。
當測試完成後,就可開始去修正bug,修完bug再跑一次測試,看是否可變成綠燈,如果是綠燈,那不僅bug修好,程式也多了一支測試,下次換人改,也就不怕又改出相同的bug了。

2012年4月14日 星期六

來空空,去空空

保持乾淨是Unit Test 的原則之一。
怎麼說呢? 進行測試前,先要有乾淨的環境測試結果才會正確。當測試完後,也要把環璄整理乾淨,留給下個測試者乾淨的空間。

保持乾淨,常常用在對資料庫的資料作測試。進行測試前,要先確保測試Table是空的,然後才開始測試。測試的過程中可能會 Insert 資料或 Update 資料,當測試結束後,就把測試Table中的資料清空。這樣,下一個 Unit Test 才不會被先前測試時所遺留的資料干擾。

如何在每次測試前都有乾淨的Table 呢?

第一種作法就是在每次測試完後,使用SQL 刪除資料,不過這種作法會很累人。

第二種作法就是在每支測試方法上再包一個Transaction,當測試完成後Transaction 自動Rollback,這樣就不會有任何資料被存入,測試Table永遠保持乾淨。

使用Transaction 自動 Rollback 是最佳的方法,可以使用Spring 的 SpringJUnit4ClassRunner 進行設定。先寫一個SpringTXBaseTest,如果有需要Transaction Rollback 的Test 就只要繼承 SpringTXBaseTest 即可。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"/applicationContext*.xml"})
@TransactionConfiguration(transactionManager="transactionManager", defaultRollback=true)
@Transactional
public abstract class SpringTXBaseTest {}

public class DaoTest extends SpringTXBaseTest {

@Autowired
private MemberDao dao;
 
@Test
public void testSelCountByAcc(){
  
String acc="7R6";
ClubMember member=OM.createPersistClubMember(acc);
dao.saveOrUpdate(member);
assertTrue(dao.selCountByAcc(acc)>0);
  
}
}

在DaoTest中的每支Test都有包Transaction,所以testSelCountByAcc()執行完後,就會自動Rollback。如果好奇想要看看資料是否真的可寫入資料庫,只要加上@Rollback(false)即可。確定資料可寫入資料庫後,記得要把資料刪除保持乾淨。

@Rollback(false)
@Test
public void testSelCountByAcc(){
  
String acc="7R6";
ClubMember member=OM.createPersistClubMember(acc);
dao.saveOrUpdate(member);
assertTrue(dao.selCountByAcc(acc)>0);
  
}

2012年4月3日 星期二

測試 For 迴圈不要忘了測Size 大小

一般測試 Collection 的資料如 List, Set 都會使用迴圈來進行測試。如果你的測試碼如下,可能就會有問題
//測試 getBigHouses()所取出的房子都是>50坪的房子
@Test
public void testHousePing(){
  
 List<House> houses=service.getBigHouses();
  
 for (House house : houses) {
   assertTrue(house.getPing().intValue()>50);
 }  
}

乍看之下好像沒有錯誤,但是如果getBigHouses() 本身就寫錯了,根本就沒有取出任何House,這時跑這支Test 仍舊會過,因為 service.getBigHouses() 取出的 houses 是空的 ,所以根本沒有進入迴圈,因此Test 再沒有任何的assert 下就過了。

所以在進行迴圈測試時,一定要先測Size,確定有資料取出,才不會造成空迴圈測試仍過的誤判。


//測試 getBigHouses()所取出的房子都是>50坪的房子
@Test
public void testHousePing(){
  
 List<House> houses=service.getBigHouses();
 assertTrue(houses.size()>0); //一定要先測size
 for (House house : houses) {
   assertTrue(house.getPing().intValue()>50);
 }  
}

你開發的程式好測試嗎(七)?

第六種情況

測試  private method.

因 private method 不好測,若要進行單元測試可採用以下幾種方式:
1. 透過 public method 間接呼叫 private method 來測試.
2. 如果private method 可以改成default method ,只要source class 和test class有相同的package 名稱,即可對default method 進行測試。
3. 使用reflection 直接對private method 進行測試。

透過 public method 間接呼叫 private method 來測試


使用public method 來間接測試 private method 是最簡易的方式。但是如果public method 沒有管道取得或回傳private method 的結果,那也是無法測;另外,比較不好的原因是2個method couple 在一起,萬一將來private method 改了,為了要能測試 private method ,public method 有可能也會一起改。

例:  public method getTenant(String name) 間接呼叫 private method findByName(String name)

public Tenant getTenant(String name){
  
   return findByName(name);
}  
 
private Tenant findByName(String name){
  
    return dao.getTenantByName(name);
  
}

直接呼叫public method getTenant() 來測試
@Test
public void testPrivateMethodByPublicMethod(){
  
 String name = "Tom";
 Tenant tenant=service.getTenant(name);
 assertEquals(name, tenant.getName());
}

使用 default method 來測試


將 private method 改為default method,並將source class和 test class 的package 設成一致,便可直接對此default method 進行測試。

例:   下圖中要測試的是RentServiceImpl 的 default method  findByName(String name) 。且Test Class (DefaultMethodTest) 和 Source Class (RentServiceImpl) 的package name 皆為 rent.report

    

//default method
Tenant findByName(String name) {

 return dao.getTenantByName(name);

}

對default method 進行單元測試
package rent.report;

import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import rent.demo.Tenant;

public class DefaultMethodTest {

 private RentServiceImpl service;

 @Before
 public void setUp() {

  service = new RentServiceImpl();  
 } 
 
 @Test
 public void testPrivate2DefaultMethod() {
  String name = "Tom";
  Tenant tenant = service.findByName(name);
  assertEquals(name, tenant.getName());
 }

}

使用reflection 直接對private method 進行測試

最後,如果對上述二種測試方式都不滿意,那就只能利用reflection 的方式直接對private method進行測試。如果不想自己寫reflection 的程式碼,最方便的方法就是去下載 junit-addons  可以經由  PrivateAccessor 的 invoke() 來呼叫 private method.

invoke(Object object, String methodName, Class[] parameterTypes, Object[] args) 
第一個參數 object: 有此待測方法的物件. 
第二個參數 methodName:待測方法
第三個參數 parameterTypes:傳入待測方法的參數型態
第四個參數 args :傳入待測方法的參數值

以 RentServiceImpl 的 findByNme(String name) 為例,第一個參數就是 RentServiceImple 的 instance service;第二個參數就是 "findByName"; 第三個參數就是  new Class[] { String.class }; 第四個參數就是new Object[]{"name"}.

例: 測試  private Tenant findByName(String name)

@Test
public void testPrivateMethod() throws Throwable {

 String name = "Tom";
 Tenant tenant = (Tenant) PrivateAccessor.invoke(service, "findByName",
    new Class[] { String.class }, new Object[] { name});
  
 assertTrue(tenant.getName().equals(name));
}
最後要注意的是,當進行refactory 將此private method name變更時,因invoke 的第二個參數是使用method 的字串名稱,所以不會隨refactory 同步更換,要自己手動將此method 名稱變更為refactory後的名稱才行。

2012年1月9日 星期一

你開發的程式好測試嗎(六)?

第五種情況:
Method中有使用到Thread.

若程式有使用Thread進行非同步運算該如何測試呢?有時侯遇到Thread的狀況很複雜就非常不好測。以下就舉一個簡單的例子來做Thread的測試。

假設有一台樂透機,當主持人按下開始後,就交由另一支Thread去進行選球的動作,該Thread要在一定的時間內從50顆球中選出5顆球。

這台樂透機 (LottoMachine)有一個方法為actionRunner(),執行此方法就相當於按下開始的動作。actionRunner() 會產生一條Thread,該Thread 會去執行 LuckRunner 中的run(),在run()中會隨機挑選5顆球,程式碼如下:
public class LottoMachine {
 
        //選取到的幸運彩球
 private List<ball> winBalls;  
        //開始執行,並由另一支Thread去進行選球的動作      
 public void actionRunner() throws InterruptedException {
  winBalls = new ArrayList<ball>();
  Thread t1 = new Thread(new LuckyRunner(winBalls));
  t1.start();
 } 

 public List<ball> getWinBalls() {
  return winBalls;
 }

 }


public class LuckyRunner implements Runnable {

 private List<ball> winBalls;;
 private Ball ball;
 private List<integer> ballNum = new ArrayList<integer>();
 {
  for (int i = 1; i < 50; i++) {
   ballNum.add(i);
  }
 }

 public LuckyRunner(List<ball> winBalls) {
  this.winBalls = winBalls;
 }

         //選取5個不重複號的球.先暫停500ms再執行,摸擬洗球的程序。
 @Override
 public void run() {

  try {
   Thread.sleep(500);
   for (int i = 0; i < 5; i++) {
    int numberIndex = (int) (Math.random() * (49 - i));
    int selNumber = ballNum.remove(numberIndex);
    ball = new Ball();
    ball.setBallNuber(selNumber);
    winBalls.add(ball);
   }

  } catch (InterruptedException e) {
   e.printStackTrace();
  }

 }

}


接下來要來撰寫測試。我們希望樂透機都能順利選出5顆彩球,號碼介於1~50之間,測試碼如下:

@Test
 public void testActionRunner() throws InterruptedException {

  LottoMachine machine = new LottoMachine();
  machine.actionRunner();  
  assertTrue(machine.getWinBalls().size() == 5); //will fail here!
  for (Ball ball : machine.getWinBalls()) {
   assertTrue(ball.getBallNuber() > 0 && ball.getBallNuber() <= 50);
   System.out.println("ballNum=" + ball.getBallNuber());
  }

 }

測試結果是Fail,因為在測試時選球的動作還在進行中,所以在machine.getWinBalls().size()是0,不是我們預期的5。這時候我們可以先讓測試這條Thread先暫停1秒中再來測試結果,這樣測試就會通過。所以我們在上述的測試碼中加上
Thread.sleep(1000)。
@Test
 public void testActionRunner() throws InterruptedException {

  LottoMachine machine = new LottoMachine();
  machine.actionRunner();
  Thread.sleep(1000);
  assertTrue(machine.getWinBalls().size() == 5);
  for (Ball ball : machine.getWinBalls()) {
   assertTrue(ball.getBallNuber() > 0 && ball.getBallNuber() <= 50);
   System.out.println("ballNum=" + ball.getBallNuber());
  }

 }

如果不想用Thread.sleep(),可以改寫用Callable 和FutureTask,使用FutureTask 的isDone()來取代Thread.sleep(),也是不錯的作法。以下為改寫的程式碼:



public class LottoMachine {

 
 private List<Ball> winBalls; 

        //回傳FutueTask,之後可用來檢查Thread的動作是否完成
 public FutureTask<List<Ball>> actionCallabler() {

  winBalls = new ArrayList<Ball>();
  Callable<List<Ball>> callabler = new LuckyCallabler(winBalls);
  FutureTask<List<Ball>> future = new FutureTask<List<Ball>>(callabler);
  Thread t = new Thread(future);
  t.start();

  return future;
 }

 public List<Ball> getWinBalls() {
  return winBalls;
 }

}
public class LuckyCallabler implements Callable<List<Ball;>;> {

 private List<Ball> winBalls;
 private Ball ball;
 private List<Integer> ballNum = new ArrayList<Integer;>();
 {
  for (int i = 1; i < 50; i++) {
   ballNum.add(i);
  }
 }

 public LuckyCallabler(List<Ball;> winBalls) {

  this.winBalls = winBalls;
 }

 //選取5個不重複號的球.先暫停500ms再執行,摸擬洗球的程序。
 @Override
 public List<Ball> call() throws Exception {

  try {
   Thread.sleep(500);
   for (int i = 0; i < 5; i++) {
    int numberIndex = (int) (Math.random() * (49 - i));
    int selNumber = ballNum.remove(numberIndex);
    ball = new Ball();
    ball.setBallNuber(selNumber);
    winBalls.add(ball);
   }

  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  return winBalls;
 }
}


接下來進行unit test,因為使用FutureTask,所以可以由外部介由FutureTask的isDone 來了解Thread是否已執行完成。若Thread己執行完成,再來進行測試,就可測試Thread的執行結果,測試碼如下:

@Test
 public void testActionCaller() {

  LottoMachine machine = new LottoMachine();
  FutureTask<List<Ball>> future = machine.actionCallabler();
                //使用While 來等待,當future.isDone為true,則進行驗證,驗證完則立刻break;否則會進入無窮迴圈
  while (true) {
   if (future.isDone()) {
    assertTrue(machine.getWinBalls().size() == 5);
    for (Ball ball : machine.getWinBalls()) {
     assertTrue(ball.getBallNuber() > 0
       && ball.getBallNuber() <= 50);
     System.out.println("ballNum=" + ball.getBallNuber());
    }
    break;
   }

  }
 }

2012年1月2日 星期一

你開發的程式好測試嗎(五)?

第四種情況:
Method使用Singleton物件!

所謂Singleton模式就是獨一物件模式,若一個Class 已經有一個Instance,就無法再new 出第二個Instance。例如:系統只有一個加密器叫OnlyOneCipher,因為不想讓使用者能自行new 出一個新的加密器,所以就把OnlyOneCipher設計成一個Singleton物件。程式碼如下: 
public class OnlyoneCipher {

 private static OnlyoneCipher cipher;

 private OnlyoneCipher() {}

 public static OnlyoneCipher getInstance() {

  if (cipher == null) {
   cipher = new OnlyoneCipher();
  }
  return cipher;
 }
 
 
 public String encrypt(String source){
                 
  return doEncrypt(source);
 }

}

因為是使用Private 的Constructor(),所以只有自已能夠產出OnlyoneCipher 的Instance。外部要取得OnlyoneCipher 的Instance只能經由getInstance()這個static method。


接下來我們來看其它物件會如何引用這個OnlyoneCipher。假設有一支MessageService是用來做訊息傳遞,當要傳遞加密訊息時需經由encryptMsg()來處理。程式碼如下:
public class MessageService {
  

 public String encryptMsg(String source){
    
  return OnlyoneCipher.getInstance().encrypt(source);  
  
 }

}

因為只能經由getInstance()才可取得OnlyoneCipher物件,所以在程式中很直覺的寫法就是OnlyoneCipher.getInstance(),當引用
OnlyoneCipher的地方越多,則OnlyoneCipher.getInstance()就會散到整個應用程式中。當有一天想要換更新的Cipher,這時候麻煩就來了,除非要把所有的OnlyoneCipher.getInstance()置換,否則換不掉。另外,在進行unit test 也很難測,因為OnlyoneCipher.getInstance()這種寫法等於是跟encryptMsg()綁死,抽換不掉,如果這個OnlyoneCipher又跟底層或是環境如KEY綁在一起,那就更難測了,在測試環境下,OnlyoneCipher也許會無法正常運作,而Singleton取得物件的模式,會使得測試更難進行。

當然,Singleton 還是可以測,只是寫法要變,而且要使用Mock Framework。因為Singleton 有用到Private Constructor 所以無法extend,因此就無法使用subclass Mock,要使用CGlib 動態產生Mock Class,這時就可以使用JMock、EasyMock、Mockito 等Framework 。利用Mock 來測試Singleton 的程式碼可以改寫如下:


public class MessageService {  

  private OnlyoneCipher cipher;  
  
  
 public void setCipher(OnlyoneCipher cipher) {
  this.cipher = cipher;
 }


 public String encryptMsg(String source){
    
  return cipher.encrypt(source);  
  
 }

}

使用EasyMock 對OnlyoneCipher 做Mock,利用此Mock Inject 到MessageService中,以便進行單元測試!

public class SingletonTest {
 
 private MessageService service;
 private OnlyoneCipher cipher; 
 
 @Before
 public void setup(){
  
  service=new MessageService();
        //對OnlyoneCipher 做Mock
  cipher=createMock(OnlyoneCipher.class);  
  service.setCipher(cipher);
 }
 
 
 @Test
 public void testEncrypt(){  
  
  String source="Test String";
  String encode="aaBBddaa";  
  expect(cipher.encrypt(source)).andReturn(encode);
  replay(cipher);
  String msg=service.encryptMsg(source); 
  verify();
  assertEquals(encode,msg);
 }

}


如果非必要,儘可能少用Singleton 模式,如果想要產生獨一物件,不見得要用Singleton Pattern,可以透過Spring 來提供獨一物件。雖然Spring 控管的Bean 使用者還是可自行new ,但是比起Singleton 模式好做多了。


所以,程式用了很多Singleton,將來要置換會很麻煩而且測試也不容易,難怪會有人說 "Singleton Is Evil"。

2011年12月26日 星期一

你開發的程式好測試嗎(四)?

第三種情況:
在Method中有new 底層的物件或服務。這些物件可能必須配合系統的環境才可生成,在測試環境也許生不出這些物件。

假設公司有一個遠端服務(remote service),當輸入計畫編號時,該服務會回傳"計畫"(Project)這個物件。因這項遠端服務具有機密性,所以只能在特定的環境下使用,在開發或測試環境是無法呼叫得到。另外這個服務的相關Class和設定都已被包在一個 Jar 檔裏。要使用這項遠端服務,只要呼叫相關的API即可,其它設定都不需更動。

這個遠端服務有一個Interface 為IRemote,並有一個getProject(String prjNo) 方法。程式碼如下:
public interface IRemote {
 
  Project getProject(String prjNo);

}

在自行開發的程式中有一項服務(AppService),需透過遠端服務來取得計畫預算,所以程式碼可能會這樣寫:
  public class AppService implements IService{
   
    public Long getPrjBudget(String prjNo){  
       IRemote remote=new RemoteProxy();
       Project project=remote.getProject(prjNo);
       return project.getBudget();
   }
}
如果程式碼是這樣寫,那測試就不會過,因為測試環境根本無法呼叫到遠端服務,而且在測試環境中,程式也跑不起來。程式中,IRemote remote=new RemoteProxy() 就跟getPrjBudget(..) 這個方法綁死無法抽換。只要在方法中有用到 new 來建立服務,那這個服務就會跟這個方法綁死而無法抽換。

如果我們把程式改寫如下:
public class AppService implements IService{
   
   private IRemote remote;
  
   public void setRemote(IRemote remote){
     this.remote=remote;
  }
 
   public Long getPrjBudget(String prjNo){  
      
       Project project=remote.getProject(prjNo);
       return project.getBudget();
   }
}

這樣遠端服務就沒有跟getPrjBudget()這個方法綁死,而是變得靈活可抽換,當測試時就可以提供假的遠端服務來測試程式的邏輯,這樣單元測試就能執行,而且在測試環境中也能夠將程式跑起來。

依環境取得AppService:
  public AppService getService(){
    
    AppService service=new AppService();
    if(isDevelopment()){
     //取得測試環境的遠端服務 
     service.setRemote(getMockRemote());
   }else{
     //取得正式環境的遠端服務
     service.setRemote(new RemoteProxy());
   }
    return service;
}


AppService 的單元測試:

 public class AppServiceTest{

 private AppService service;

 @Before
 public void setup(){
  service=new AppService();
  service.setRemote(getMockRemote());

 }   

 @Test
  public void testGetPrjBudget(){
   
   String prjNo="A123";
   Long budget=service.getPrjBudget(prjNo);
   assertTrue(budget>0);
}
  
}

2011年12月19日 星期一

你開發的程式好測試嗎(三)?

第二種情況:
傳入到Method的參數,型態為底層的Interface。

以Spring 3.0 MVC 為例:
假設有一個RentController,該Controler 有一個 tenantPage() Method ,傳入Metod 的參數皆為底層的Interface 。程式碼如下:

@Controller
@RequestMapping("/rent")
public class RentController {
 
 @Autowired
 private IRentService service;


 @RequestMapping(value = "/tenant", method = RequestMethod.POST)
 public String tenantPage(Model model,HttpServletRequest request) {

   Integer id=Integer.valueOf(request.getParameter("id"));
   Tenant tenant=service.getTenant(id);
   model.addAttribute("tenant", tenant);
  return "tenant";
 }
 
 
 public void setService(IRentService service) {
  this.service = service;
 }

}

其中Method 參數: model 的型態為org.springframework.ui.Model;request 的型態為javax.servlet.http.HttpServletRequest。 二個參數型態都是Interface,當撰寫Unit Test時會出現一個狀況,必需要實作這些Interface才能將參數傳入。為了測試而去實作這些複雜的Interface是沒有效率的,如果下次碰到不同的Interface 又要再實作一次,那就不會有人想再去寫Unit Test 了。

@Test
 public void testRentController(){
  
  RentController rc=new RentController();
  rc.setService(new RentServiceImpl());
  //model 和 HttpServletRequest 都是Interface,new 不出來
  Model model=yourModelImpl();
  HttpServletRequest request=yourRequestImpl();
  String pageName=rc.tenantPage(model, request);
  Assert.assertEquals("tenant",pageName);
  
 }

所以當測試遇到底層的Interface 時,可以有2種作法。

第1種作法: 如果Methd中的核心邏輯不會和底層的Interface有相依性,則可將此核心邏輯抽取出來成一個Method,Unit Test就針對這個Method 來測試。例如:在 tenantPage()中,service.getTenant(id)是主要的邏輯且沒有和底層Interface有交互作用,因此可將 service.getTenant(id) 抽取出一個Method 名為getTenant(Integer id)。這時getTenant(Integer id)就很單純 ,只有一個型態為Integer 的id,相對的也比較好進行 Unit Test 。

@RequestMapping(value = "/tenant", method = RequestMethod.POST)
 public String tenantPage(Model model,HttpServletRequest request) {

   Integer id=Integer.valueOf(request.getParameter("id"));
  //Tenant tenant=service.getTenant(id);
   model.addAttribute("tenant", getTenant(id));
  return "tenant";
 }
 
 //new extract method
 public Tenant getTenant(Integer id){
  
  return service.getTenant(id);
 }

第2種作法: 如果核心邏輯無法和底層的Interface做有效切割,這時就只能使用Mock Framework。
利用Mock 來模擬底層物件,而不必自己辛苦去實作這些Interface。有關Mock的測試方法,未來會再詳細述說。以下的測試碼是使用EasyMock Framework,來模擬Model 和 HttpServletRequest。


public class RentControllerTest {

 private RentController rc;
 private RentServiceImpl mockService;

 @Before
 public void setUp() {

  rc = new RentController();
  mockService = createMock(RentServiceImpl.class);
  rc.setService(mockService);
 }

 @Test
 public void testRentController() {

  //test prepare
  String key = "id";
  String idVal = "2";
  String attName = "tenant";  
  Tenant tenant = getTenant();
  HttpServletRequest mockRequest = createMock(HttpServletRequest.class);
  Model mockModel = createMock(Model.class);
  //expect mock behavior
  expect(mockRequest.getParameter(key)).andReturn(idVal);
  expect(mockService.getTenant(Integer.valueOf(idVal))).andReturn(tenant);
  expect(mockModel.addAttribute(attName, tenant)).andReturn(mockModel);
  //replay mock behavior
  replay(mockRequest);
  replay(mockService);
  replay(mockModel);
  //action start
  String pageName = rc.tenantPage(mockModel, mockRequest);
  //verify mock behavior
  verify(mockRequest);
  verify(mockService);
  verify(mockModel);
  //assert test result
  Assert.assertEquals(attName, pageName);
 } 

 private Tenant getTenant() {
  Tenant tenant = new Tenant(Identity.GENERAL);
  tenant.setAge(31);
  tenant.setCareer("IT");
  tenant.setName("John");
  return tenant;
 }
}

使用Mock就可以把複雜的介面切開,而且也可以擺脫環境的限制。上例中的測試碼包含了許多Expect ,其目的是要檢查是否有執行到這段程式碼。所以,Mock就相當於白箱測試,白箱測試會比黑箱測試複雜,你看測試碼是不是變得比較多了呢!

2011年12月12日 星期一

你開發的程式好測試嗎(二)?


上一篇提到6種較不易進行測試的情況。接下來就來細說為何難測的原因及重構方法。

第一種情況:
 程式無聲無息的做完了,沒有提供任可訊息可供判斷!

這種情形,就好像太空船進到黑洞,完全無法回傳任何訊息一樣。無法取得訊息,就很難進行測試。當然還是能由一些間接方法來推測,但這樣做很容易因環境變更,而使得Unit Test 失敗。最好的方法還是程式能夠提供適當的訊息供Unit Test 做判斷。

假設有一支 ReportAgent,它可產生 Excel 和 PDF 報表。程式碼如下:

public class ReportAgent {

        //產生房客 Excel 報表資料 
 public boolean createExcelReport(Tenant tenant) throws IOException {

  if (tenant != null) {
   HSSFWorkbook wb = new HSSFWorkbook();
   HSSFSheet sheet = wb.createSheet();
   HSSFRow row = sheet.createRow(0);
   row.createCell(0).setCellValue(tenant.getName());
   row.createCell(1).setCellValue(tenant.getAge());
   row.createCell(2).setCellValue(tenant.getCareer());
   FileOutputStream out = new FileOutputStream("report.xls");
   wb.write(out);
   out.close();
   return true;
  } else {
   return false;
  }
 }

        //產生房客 PDF 報表資料 
 public boolean createPdfReport(Tenant tenant) throws FileNotFoundException,
   DocumentException {

  if (tenant != null) {
   Document document = new Document();
   PdfWriter.getInstance(document, new FileOutputStream("report.pdf"));
   document.open();
   document.add(new Paragraph("Name:"+tenant.getName()));
   document.add(new Paragraph("Age:"+tenant.getAge()));
   document.add(new Paragraph("Career:"+tenant.getCareer()));
   document.close();
   return true;
  } else {
   return false;
  }
 }
 
 //產生房客 Word 報表資料
 public boolean crateWordReport(Tenant tenant)  throws IOException{  
  //TODO
  return true;
 }

}
使用者需經由RentServiceImpl 才能產生房客報表。報表型態(ReportType)有三種,分別為Excel、PDF、WORD。但是房客報表只提供EXCEL和PDF二種報表的選擇,並不提供WORD報表。程式碼如下

public enum ReportType {

 EXCEL,PDF,WORD
}

public class RentServiceImpl implements RentService {

 private ReportAgent reportAgent;

 public void setReportAgent(ReportAgent reportAgent) {
  this.reportAgent = reportAgent;
 }

 @Override
 public void createReport(Tenant tenant, ReportType type)
   throws IOException, DocumentException {

  if (type == ReportType.EXCEL) {
   reportAgent.createExcelReport(tenant);
  } else if (type == ReportType.PDF) {
   reportAgent.createPdfReport(tenant);
  }
 }

}
當對 RentServiceImpl 中的createReport()進行測試時,無法得知是否執行正確。例如:當傳入的ReportType 為PDF 時,是否真的產生PDF報表,如果傳入的ReportType 為WORD,程式也不會有任何反應,所以這個測試只要不出Exception,所有的條件都會過,那這就不是一個有效的測試。測試碼如下

   public class RentServiceTest { 
 
 private RentServiceImpl service;
 
 @Before
 public void setUp(){
  
  service=new RentServiceImpl();
  service.setReportAgent(new ReportAgent());  
 } 
 
 //無效的測試,無法判別錯誤
 @Test
 public void testCreateReport() throws IOException, DocumentException{
  
  service.createReport(getTenant(), ReportType.PDF);
  service.createReport(getTenant(),  ReportType.WORD);
 } 
  
 private Tenant getTenant() {
  Tenant tenant=new Tenant(Identity.GENERAL);
  tenant.setName("Jim");
  tenant.setAge(30);
  tenant.setCareer("Doctor");
  return tenant;
 }

}
接下來,要來重構createReport(),當某個型態的report有被順利產出時,就回傳這個型號代號,若沒有這個型態的report就丟Exception。程式碼如下:

public ReportType createReport(Tenant tenant, ReportType type)
   throws IOException, DocumentException, ReportException {

  if (type == ReportType.EXCEL) {
   reportAgent.createExcelReport(tenant);
   return ReportType.EXCEL;
  } else if (type == ReportType.PDF) {
   reportAgent.createPdfReport(tenant);
   return ReportType.PDF;
  }else {
   throw new  ReportException("Format Not Support");
  }
 }

        @Test
 public void testCreateReport() throws IOException, DocumentException, ReportException{
  
  ReportType reportType=service.createReport(getTenant(), ReportType.PDF);
  Assert.assertEquals(ReportType.PDF, reportType);
  try {
   service.createReport(getTenant(),  ReportType.WORD);
   Assert.fail();
  } catch (Exception e) {
   
   Assert.assertTrue(true);
  }
 } 
這樣,測試程式可依回傳值來判斷是否有產生正確的Report。如果有傳入錯誤型態,程式也會以Exception 的方式告知。有了這些訊息,測試程式就能驗證邏輯是否正確。

2011年11月27日 星期日

你開發的程式好測試嗎(一)?

有寫過單元測試(Unit Test)的開發人員因該都有這種感覺,"測試碼比程式碼還要難寫"。要讓所設計的程式可自動被測試,這可是一門學問;更進階的測試手法還可搭配Mock或Reflection來達成。今天先談基本的單元測試,進階的測試未來再詳述。

1. 單元測試是各自獨立的
單元測試是沒有依賴性或先後順序的。如果測試程式彼此間有互相依賴,就不易進行自動化測試,因為在執行自動化測試時,通常是隨機進行,而不會按照一定的順序去執行測試。當然,有的測試會按測試名稱的英文字母順序來執行,但是當接手的人把測試名稱改了,之前所調好的順序又會大亂;再來測試有依賴性,容易因某人改了程式,而造成其它的測試程式測試失敗。

2.單元測試的範圍是有限制的 例如:只測DAO這層或只測Service這層,如果Service有呼叫DAO這層,那在測Service這層時,也會再測一次DAO,這樣就跨了二層,而不是一個單元了。如果DAO已經測試過,每次測Service層時,是否還要再把DAO拉進來一起測呢?如果拉進來測,那又會跟Database綁在一起,測起來就很沒效率。
如果要讓Service層做到所謂的單元測試,可以使用Mock來模擬DAO的行為,將DAO  Mock  Inject 到Service 中,這樣就可把DAO切開,單純地測試Service 的邏輯。因為測試Service層時,不會再和Database 連結,所以測試的速度可以快上一倍。以前曾參與開發一個大型系統,這個系統有近2千支的測試程式,全部跑完CI測試需45分鐘,之後我們將Service中的DAO改為Mock,則CI只要花20分鐘就跑完所有測試。所以Database是關鍵,使用Mock 將Database 和Service分離,大大的提升CI的效率。
測試包含了黑箱測試、白箱測試、Web 測試、整合測試、Javascript 測試、Stored Procedure 測試、壓力測試、安全性測試和使用者測試...等。除了使用者測試外,其它都可撰寫測試碼進行自動化測試,只要程式能夠被自動化測試,程式的品質和設計就比較不會有問題。
要寫好單元測試是要一點時間,當你發現程式很難測的時候,就代表你可能要變更設計了。程式不易被單元測試的常見的情況有:

1. 程式無聲無息的做完了,沒有提供任何訊息可供判斷!

2. 傳入到Method的參數,型態為底層的Interface!因為跟底層綁在一起,不好測!

3. 在Method中有new 底層的物件或服務。這些物件可能必須配合系統的環境才可生成,在測試環境也許生不出這些物件。

4. Method中有使用Singleton物件。 (Singleton Is Evil,少用)!

5. Method中有使用非同步運算,測試都是針對Current Thread,非同步的Thread要由另一個測試程式來測。如果非同步是和Mail Server 有關,只能測到信有送出,至於是否有收到信,就不是單元測試的範圍了。

6. 你想測的是 Private Method.  若是.NET 開發人員,Visual Studio 可自動利用Reflection 的機制來對Private Method 進行測試,JAVA的開發人員,就沒有這麼好的工具。當然,還是有方法可以測,只是比較麻煩!


待續...

2011年11月20日 星期日

什麼是TDD(Test Driven Development)

TDD(Test Driven Development),中文有人稱為"測試先行"。意思就是,在開發程式時,要以測試的角度來設計。以前若不是使用TDD的方式來設計程式架構,所設計出來的API可能是無法經由測試程式來進行自動測試。如果程式無法被測試,那就是很不好的設計。
舉個例子來說,家裏的電燈不亮,如果在有電的情況下,不是底座壞了,就是燈泡壞了。這時侯你可能先把燈泡拆下,放到沒有問題的底座上,測試看看燈炮是否有問題,如果燈泡亮了,則代表問題不在燈泡(可能是底座或電線等問題),若燈泡不亮,則肯定是燈泡壞了。所以燈泡是可測試的零件;如果有廠商把燈泡和底座封起來,變成一體成型的話,一旦那天電燈不亮,不僅無法個別測試,也無法單獨更換燈泡,每次壞了就要底座和燈泡一起更換,那這種電燈就是不好的設計。從這個例子中,是否有隱約感受到開發程式時常講的Coupling(耦合)和Decoupling(低耦合),現在越來越多的Framework都強調是否好測試,越是POJO和Decoupling的架構,這個Framework就越好測試。以前Struts 的Action 就因綁了底層的API,所以很難測,之後的Struts2就變得可測試,再來Spring 的MVC 更是POJO 和Decoupling的代表作。所有的Framework 演變到如此,就是為了要能夠測試。不能夠測試的Framework終將會被淘汰。
你設計的API是可以測試的嗎?我以計算租金的API為例,以TDD的方式來進行設計。
租金計算的邏輯如下:
1.租金是依地區和坪數做基本計算。
2.如果房客是公司,因公司要扣繳10%的稅金,房東會提高租金。就是將租金再除以0.9。
3. 如果房客是學生,因考量經濟能力,則以8折優待。

我以JAVA為程式語言,以Eclipse 做為開發的IDE,以JUnit 4為單元測試的Framework。

首先建立一個Project 名為 RentDemo,並有2個Source Folder 分別為"src" 和 "test"。(如圖一)









圖一.



要以TDD的方式來開發,所以就先寫測試。我想有人應該會問,租金的API都沒寫那要怎麼測?話不多說,馬上實作。
1.在test 的的rent.demo package 中建一個Class 名為FirstRentTest。(如圖二)
2. 在FirstRentTest Class 中建一個method 名為 testCalRent。(如圖三)












圖二.











圖三.



3. 開始設計。租金是因為有房子供出租而得,所以會先有House 這個物件,而計算租金calRent 這個method,就由House 這個Domain 物件來提供。再來,坪數(ping)和房客(tenant)和地區(region)都可做為House 的屬性。有了以上的概念後,就可以開始寫Code。























圖四.


4. 建立 myHouse 物件,因還沒有定義House這個Class,所以Eclipse 出現錯誤的紅色X號(如圖四)。請用滑鼠左鍵點選紅色X號,即會出現中間這個對話視窗,請點選Create class 'House',這時會出現另一個對話視窗(如圖五),請將Source Folder 更改為"RentDemo/src",package 不變,按下"Finish"後,就會在"src" 的rent.demo package中建立一個House.class。(如圖六)


(圖五.)

(圖六.)

5. 在House.class 中建立坪數(ping)、房客(tenant)和地區(region) 這3個屬性。坪數可能會有小數所以使用BigDecimal 型別,房客使用 Tenant 型別,地區使用Region 型別。因尚未在程式中定義Tenant 和 Region型別,所以Eclipse 出現紅色X號,同樣以上述手法分別建立Tenant 和 Region 型別,最後在House.class 中將各屬性的 getXXX() 和 setXXX() 建立好。 

(圖七.)

(圖八.)

6. 都建立好後,再回到 FirstRentTest,繼續完成測試碼。將各屬性值設給House 物件,而且House  物件要提供一個計算租金的方法(calRent())。同樣的,因House 物件中尚未定義calRent()這個方法,所以Eclipse 又出現紅色X號(如圖九)。將滑鼠左鍵點選紅色X號,再點選"create method calRent()in type House",便會在House.class 中建立calRent()的方法。

(圖九.)

7.開始假設情節,如果房子有35坪,且座落在大安區,大安區的每坪租金假設為1200元,而房客的身份為一般大眾,則租金=35*12000=42,000元。所以可以為這個情節開始進行測試,啟動測試後,出現紅燈,表示測試未通過,這是正常的,因為我們還未在calRent()撰寫任何程式碼,如果這時測試出現綠燈,那就奇怪了,一定那裡出了問題。(如圖十)

(圖十.)

8. 接下來,只要我們能將測試的紅燈轉變成綠燈就代表我們將calRent()這個method完成,並且通過測試。在計算租金時,我希望由Region物件提供該地區的每坪租金價格,所以Region 就會有2個屬性,分別是name(地區名稱)和 price(每坪租金價格),因為"地區名稱"和"每坪租金價格"是較不易變動的屬性,所以我想以Constructor的方式來將屬性值設給Region 物件。

(圖十一.)

9.再來換房客(Tenant),因房客有三種身份(公司,學生,一般),所以房客會有一個kind(類別)屬性,型別為Enum。再來,房客可以提供各種身份的係數值,以便提供租金計算。例如: 公司為(10/9),學生為(0.8),一般為(1.0)。也是以Constructor的方式將身份屬性設給房客。







(圖十二.)


(圖十三.)

10. 再將測試修正(如圖十四)


(圖十四.)

15. 開始撰寫 calRent()。租金=坪數*每坪租金價格*房客係數,因calRent()的回傳值為double,所以用BigDecimal 來做乘法運算,才不會有精確度不準的問題。
(圖十五.)
16. 回到FirstRentTest 再測試,看是否變成綠燈,如果是綠燈則代表在房客為"一般"的情況下,程式計算正確沒有問題。最後再把房客為公司和學生的情況也進行測試,若都是綠燈,則代表租金計算的API已完成,且通過各種情境測試。


(圖十六.)

最後,以TDD的方式開發還要看IDE是否有支援,有些IDE就不能從Pseudo Code中建立Class 或Method。另外使用敏捷(Agile)開發流程如XP(Extreme Programming)或Scrum 是比較有機會使用TDD的開發方式。如果是使用傳統的瀑布式的開發流程,先等文件寫好再開發,那麼設計就不容易變更,TDD的開發也較難落實。

如果沒有使用TDD的方式來開發calRent(),那麼程式碼會是什麼樣呢?可能會是一堆if..else 塞滿整個程式,如果有新的房客身份,又要加一個if判斷。因為思考的方試不同,所以寫的方式也就不同。使用TDD,會以好測試的角度來設計程式,所以程式就會設計的較乾淨,大家可以嘗試寫寫看。

抗拒、習慣、不能沒有測試

我從2003年開始接觸單元測試(Unit Test)和持續整合(Continuous Integration,CI)。在這麼多年的實戰經驗中,深刻的感受到測試所展現的威力和種種的好處。之前不論是在JAVA或.NET團隊,都不斷宣揚測試是我們的護身符;只有被測試解救的人才會真正地了解,它才是原碼的背後英雄。

2003年時我在中研院進行實驗室資訊管理系統(LIMS)的開發。因專案很大,有請顧問導入流程和Framework,包含Struts,Hibernate,Spring,Unit Test 和 CI。而我的第一支測試碼也是在不情願和抗拒的情況下寫出來的。為何會不情願呢?當時認為還要多花時間寫測試碼,而且測試碼有時候不好寫,久了就開始偷懶,最後乾脆不寫了。 這也是大多數團隊遇到寫測試時常有的通病,有時組員三不五時還會抱怨,時程已經很趕了,可否能省略測試碼。可以,但是這個測試債有一天一定要還,還的人也許不是你,而是下一個倒楣鬼。

開發團隊在導入單元測試時,一定要有一位導師來負責監控組員所寫的測試,那位導師要非常懂測試,而且是位堅持原則而不易妥協的人。我們當年就有一位導師綽號為"叫獸"-愛叫的猛獸. 只要你不寫測試,而且程式碼覆蓋率低於90%,你就等著被吼,那時的團隊就是這樣被逼出來的。測試寫久了,就越寫越快,最後也就習慣了,而且使用Test-Driven Development(TDD)的開發方式,開發速度就更快了。

實驗室是很精密的單位,不能容許些微的差錯。程式碼也是一樣,一旦出錯有時侯是會釀成災難的。在多人共同開發的專案中,常常某位成員將API改了,就會導致一連串的連鎖錯誤,而且細微的錯誤更是無法立即檢查出來;曾經就發生過一件事,我把自己開發計算試劑存量的API改了,自己的單元測試也測試通過,上了CI後,以為一定OK,馬上就可上線,沒想到半小時後,CI郤回報錯誤,有人的測試碼沒有過。因為某些試劑是不能使用新版API的計算方式,因為試劑太多了,我沒有想到還有這些例外情況,還好大家都有寫測試,否則一旦上線,該試劑的存量錯誤不會立即知道,可能要等1~2星期有明顯錯誤時,使用者才會查覺,等到那時已經太晚了!!

測試救了我,也讓我了解不能沒有測試!

最後就套這一句話:
剛開始你會死命地抗拒它,之後呢你會慢慢地習慣它,到最後你會發現不能沒有它!

大家是否有想過,如果我們在學習程式語言時,老師就先教我們先寫測試碼,把寫測試碼當做開發程式的標準習慣,或許現在的軟體專案品質就不會那麼差了。不幸地,現在的情況剛好相反!

PS:那位"叫獸"就是訂便當的站長