Google Code Prettify

2015年7月2日 星期四

unit test on spring + struts2

當 web 程式是用 spring + struts2 開發時,要怎麼對 action 做 unit test? 這裡提出說明,假設有個網頁如下面畫面所示:
這是查詢各選區某次選舉各候選人的得票數,當使用者選擇了「選舉場次」後,「縣市」的下拉選單要出現該次選舉所涉及的所有縣市,以供使用者選擇,這個程式的 action 如下所示:
  • action
  1 package idv.steven.demo.action;
  2 
  3 import java.util.ArrayList;
  4 import java.util.List;
  5 import java.util.Map;
  6 import java.util.TreeMap;
  7 
  8 import idv.steven.demo.dao.CandidateDAO;
  9 import idv.steven.demo.dao.ElectionDAO;
 10 import idv.steven.demo.dto.Candidate;
 11 import idv.steven.demo.dto.Election;
 12 
 13 import org.apache.struts2.convention.annotation.Action;
 14 import org.apache.struts2.convention.annotation.Result;
 15 import org.slf4j.Logger;
 16 import org.slf4j.LoggerFactory;
 17 import org.springframework.beans.factory.annotation.Autowired;
 18 import org.springframework.stereotype.Controller;
 19 
 20 import com.opensymphony.xwork2.ActionSupport;
 21 
 22 @Controller
 23 public class AreaResultAction extends ActionSupport {
 24     private static final long serialVersionUID = -213699129575326012L;
 25     final static Logger logger = LoggerFactory.getLogger(AreaResultAction.class);
 26     
 27     @Autowired
 28     private ElectionDAO electionDAO;
 29     
 30     @Autowired
 31     private CandidateDAO candidateDAO;
 32     
 33     private Map<String, String> electionList = new TreeMap<String, String>();
 34     private Map<String, String> cityNameList = new TreeMap<String, String>();
 35     private Map<String, String> areaNameList = new TreeMap<String, String>();
 36     private List<Candidate> candidateList = new ArrayList<Candidate>();
 37     
 38     private String electionID;
 39     private String cityName;
 40     private String areaName;
 41 
 42     /**
 43      * 將網頁導向「AreaResult.jsp」(「選區投票結果查詢」網頁)
 44      */
 45     @Action(value = "/areaResult", results = { @Result(location = "/jsp/AreaResult.jsp", name = "success") })
 46     public String execute() throws Exception {
 47         return SUCCESS;
 48     }
 49     
 50     /**
 51      * 將查詢結果顯示在 Grid
 52      * @return
 53      */
 54     @Action(value = "/showAreaResult", results = { @Result(location = "/jsp/AreaResultGrid.jsp", name = "success") })
 55     public String showAreaResult() {
 56         return SUCCESS;
 57     }
 58 
 59     /**
 60      * 傳回選舉場次
 61      * @return
 62      */
 63     @Action(value = "/findElection", results = { @Result(name = "input", type = "json") })
 64     public String findElection() {
 65         List<Election> list = electionDAO.findAll();
 66         for(Election item:list) {
 67             electionList.put(item.getId(), item.getName());
 68         }
 69         
 70         return INPUT;
 71     }
 72     
 73     /**
 74      * 傳回縣市
 75      * @return
 76      */
 77     @Action(value = "/findCityName", results = { @Result(name = "input", type = "json") })
 78     public String findCityName() {
 79         List<String> list = candidateDAO.findCityName(electionID);
 80         for(String item:list) {
 81             cityNameList.put(item, item);
 82         }
 83         
 84         return INPUT;
 85     }
 86 
 87     /**
 88      * 傳回選區
 89      * @return
 90      */
 91     @Action(value = "/findAreaName", results = { @Result(name = "input", type = "json") })
 92     public String findAreaName() {
 93         List<String> list = candidateDAO.findAreaName(electionID, cityName);
 94         for(String item:list) {
 95             areaNameList.put(item, item);
 96         }
 97         
 98         return INPUT;
 99     }
100     
101     @Action(value = "/findCandidate", results = { @Result(name = "input", type = "json") })
102     public String findCandidate() {
103         candidateList = candidateDAO.findCandidate(electionID, cityName, areaName);
104         
105         return INPUT;
106     }
107     
108     //由網頁中取值
109     public Map<String, String> getElectionList() {
110         return electionList;
111     }
112     
113     public Map<String, String> getCityNameList() {
114         return cityNameList;
115     }
116 
117     public Map<String, String> getAreaNameList() {
118         return areaNameList;
119     }
120     
121     public List<Candidate> getCandidateList() {
122         return candidateList;
123     }
124 
125     //網頁 submit 時傳值過來
126     public void setElectionID(String electionID) {
127         this.electionID = electionID;
128     }
129 
130     public void setCityName(String cityName) {
131         this.cityName = cityName;
132     }
133 
134     public void setAreaName(String areaName) {
135         this.areaName = areaName;
136     }
137 
138 }
使用者選擇「選舉場次」時,傳到 action 的是 electionID,「縣市」的下拉選單呼叫 findCityName 從資料庫取得所有縣市,以 json 格式傳給 jsp,jsp 再透過 getCityNameList 取得縣市資料,顯示於下拉選單中。了解整個使用者與系統間互動的流程非常重要,因為接下來要進行單元測試時,單元測試程式是要模擬成瀏覽器與這個系統互動。
  • import jar file
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${spring.version}</version>
</dependency>

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-core</artifactId>
    <version>${struts.version}</version>
</dependency>
在開始單元測試前,先在 pom.xml 中配置如上的 jar file。
  • 單元測試
 1 package idv.steven.demo.action;
 2 
 3 import static org.junit.Assert.*;
 4 
 5 import java.io.UnsupportedEncodingException;
 6 import java.util.HashMap;
 7 import java.util.Map;
 8 
 9 import javax.servlet.ServletException;
10 
11 import org.apache.struts2.StrutsSpringTestCase;
12 import org.junit.After;
13 import org.junit.Before;
14 import org.junit.Test;
15 import org.junit.runner.RunWith;
16 import org.springframework.test.context.TestExecutionListeners;
17 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
18 import org.springframework.test.util.ReflectionTestUtils;
19 
20 import com.opensymphony.xwork2.ActionContext;
21 
22 @RunWith(SpringJUnit4ClassRunner.class)
23 @TestExecutionListeners({})
24 public class AreaResultActionTest extends StrutsSpringTestCase {
25     @Override
26     protected String[] getContextLocations() {
27         return new String[] {"classpath:beans-config.xml"};
28     }
29     
30     @Before
31     public void setUp() throws Exception {
32         //這幾行一定要寫
33             //等價於在 web.xml 中設定 <init-param> 參數
34         Map<String, String> dispatcherInitParams = new HashMap<String, String>();
35         ReflectionTestUtils.setField(this, "dispatcherInitParams", dispatcherInitParams);
36             //載入 struts 設定檔
37         dispatcherInitParams.put("config", "struts-default.xml,struts-plugin.xml,struts.xml");
38         
39         super.setUp();
40     }
41 
42     @After
43     public void tearDown() throws Exception {
44         super.tearDown();
45     }
46 
47     @Test
48     public void testFindCityName() throws UnsupportedEncodingException, ServletException {
49         //初始化 http 相關環境變數 request、response ...
50         initServletMockObjects();
51         
52         //jsp要submit那些欄位的資料到action? 透過 request.setParameter傳入。
53         request.setParameter("electionID", "201201");
54         executeAction("/findCityName");
55         
56         //執行完action,取得action物件。
57         AreaResultAction action = (AreaResultAction) ActionContext.getContext().getActionInvocation().getAction();
58         //jsp網頁中,縣市的下拉選單呼叫 cityNameList 取得縣市資料。
59         Map<String, String> cities = action.getCityNameList();
60         for(String city:cities.keySet()) {
61             System.out.println(city);
62         }
63     }
64 }
  • 上面單元測試程式 22~28 行要特別注意,每個單元測試程式都要繼承 StrutsSpringTestCase 這個類別,透過這個類別單元測試程式才能模擬出 web 的環境,26~28 行 override getContextLocations 這個 method,因為 StrutsSpringTestCase 會呼叫這個 method 以取得 spring 設定檔,我的設定檔名為 beans-config.xml。
  • setUp method 必須進行一些每個 method 測試前要進行的工作,這裡如上面的注解所說明的,載入 web.xml 及 struts 設定檔。
  • 如果我們進行的單元測試要用到 http 相關的環境變數,一定要呼叫 initServletMockObjects (50行),這樣就可以使用 request、response 等變數了。
  • 53~54行,透過 request 將 electionID 傳給 action,這相當於 jsp 透過 request 將 electionID 傳給 action。
  • 57行,執行完 action 立即取得該 action 的物件,之後即可進行驗證。

沒有留言:

張貼留言