綠燈 | 紅燈

妙思資訊
顯示具有 test 標籤的文章。 顯示所有文章
顯示具有 test 標籤的文章。 顯示所有文章

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就相當於白箱測試,白箱測試會比黑箱測試複雜,你看測試碼是不是變得比較多了呢!