项目文件结构图:
椭圆框中的Jar 包是单元测试时候需要引入的。
矩形框 MainTest 每个包下一个,为 JUnit4 的 Suite 套件,其作用是执行本包下的“测试类”和子包的 MainTest。
例如:jp.co.snjp.ht.MainTest
package jp.co.snjp.ht; import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith( Suite.class ) @Suite.SuiteClasses({ jp.co.snjp.ht.orderCheck.MainTest.class, jp.co.snjp.ht.outPreconcert.MainTest.class, jp.co.snjp.ht.partOut.MainTest.class, jp.co.snjp.ht.productCheck.MainTest.class, }) public class MainTest { }由于 jp.co.snjp.ht 包下没有“测试类”,因而只需要引入“子包”的 MainTest 即可!
而,jp.co.snjp.ht.orderCheck.MainTest
package jp.co.snjp.ht.orderCheck; import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith( Suite.class ) @Suite.SuiteClasses({ CheckBarcodeTest.class, OrderConfirmTest.class }) public class MainTest { }由于 jp.co.snjp.ht.orderCheck 下没有“子包”,因而只需要引入“测试类”
————————————————————————————————
Strut2 提供了隔离容器对象的方法,因而在所有Action 的基类将其织入。
因为本项目很小,没有单独的Business 层和DAO 层,业务逻辑在 Action 中完成,SQL 操作在 SqlHelper 中完成。
为了实现单元测试隔离测试效果,这里提供了 setSqlHelper( SqlHelper sqlHelper ) 方法,这样就可以传入模拟的 SqlHelper 对象
package jp.co.snjp.ht.util; import java.util.Date; import java.util.List; import java.util.Map; import jp.co.snjp.dao.SqlHelper; import org.apache.struts2.interceptor.CookiesAware; import org.apache.struts2.interceptor.RequestAware; import org.apache.struts2.interceptor.SessionAware; import com.opensymphony.xwork2.ActionSupport; public class BaseAction extends ActionSupport implements RequestAware,SessionAware,CookiesAware{ private static final long serialVersionUID = 1L; protected Map<String,Object> requestMap; protected Map<String,Object> sessionMap; protected Map<String,String> cookieMap; /** * 查询结果集 */ protected List<Object> list; /** * SQL 执行帮助类 */ protected SqlHelper sqlHelper; public void setRequest(Map<String, Object> requestMap) { this.requestMap = requestMap; } public void setSession(Map<String, Object> sessionMap) { this.sessionMap = sessionMap; } public void setCookiesMap(Map<String, String> cookieMap) { this.cookieMap = cookieMap; } public void setSqlHelper( SqlHelper sqlHelper ){ this.sqlHelper = sqlHelper; } /** * 记录存储过程执行的日志信息 * @param start * @param name * * Date :2012-6-7 * Author :GongQiang * @throws Exception */ protected void logStroeProcedure( Date start, String name ) throws Exception{ String formatDateTime = Utils.formatDateTime( start ); String userId = (String) sessionMap.get( "user_id" ); String sql = "insert into HT_CCGC_LOG(usercode,ccgcmc,kszxsj) "+ " values ('"+ userId +"','"+ name +"','"+ formatDateTime+"' );"; sqlHelper.executeSQL( sql ); } }虽然提供了 setSqlHelper( SqlHelper sqlHelper ) 方法,但是这方法在什么时候调用呢?
为了解决这个问题,就只能把实际的业务逻辑放到 doExecute() 方法下去执行,而在 execute()方法下调用 setSqlHelper()方法,只用测试 doExecute()方法。
doExecute()方法修饰为包可见,这样就只有测试代码可以访问。代码如下:
package jp.co.snjp.ht.productCheck; import java.math.BigDecimal; import java.util.List; import java.util.Map; import jp.co.snjp.dao.SqlHelper; import jp.co.snjp.ht.util.BaseAction; import jp.co.snjp.ht.util.SpotTicketBarcodeParser; /** * 部品检查录入-Barcode扫描Action * @author GongQiang * */ public class ProductCheckBarcode extends BaseAction { private static final long serialVersionUID = 1L; private String barcode; private String backUrl; public String getBarcode() { return barcode; } public void setBarcode(String barcode) { this.barcode = barcode; } public String getBackUrl() { return backUrl; } public void setBackUrl(String backUrl) { this.backUrl = backUrl; } /** * error_0 条码不符合规则 * error_1 订单在DB中不存在 或 订单已经执行完毕 * error_2 订单区分错误 */ @Override public String execute() throws Exception { super.execute(); setBackUrl( "productCheck/scanBarcode.jsp" ); setSqlHelper( new SqlHelper() ); return doExecute(); } String doExecute()throws Exception { SpotTicketBarcodeParser parser = new SpotTicketBarcodeParser( barcode ); if( ! parser.valid() ){ return "error_0"; } queryOrderInfo( parser.getOrderNo() ); if( orderNotExist() || orderFinished() ){ return "error_1"; } if( !checkDistinguish() ){ return "error_2"; } sessionMap.put( "order_info", list.get(0) ); return SUCCESS; } void queryOrderInfo( String orderNo ) throws Exception{ String sql = "select top 1 * from iOrder_Check where " + " OrderNo='" + orderNo + "' ;"; list = sqlHelper.executeQuery( sql ); if( list == null || list.isEmpty() ){ return; } queryNameCount(orderNo); } /** * 查询订单名称 和 订单残&实收数量 * * * Date :2012-6-8 * Author :GongQiang * @throws Exception */ private void queryNameCount( String orderNo ) throws Exception{ String sql = "select sum(nqty) as usedCount from iOrder_Check "+ " where orderno='" + orderNo + "' group by orderno;"; List usedCountResult = sqlHelper.executeQuery( sql ); BigDecimal orderCount = (BigDecimal) ((Map)list.get(0)).get( "pqty" ); BigDecimal usedCount = (BigDecimal) ((Map)usedCountResult.get(0)).get( "usedcount" ); BigDecimal remainCount = orderCount.subtract( usedCount ); sql = "select itemname from iorder_operate where " + " OrderNo='" + orderNo + "' ;"; List itemNameResult = sqlHelper.executeQuery( sql ); String itemName = (String) ((Map)itemNameResult.get(0)).get( "itemname" ); ((Map)list.get(0)).put( "remaincount", remainCount ); ((Map)list.get(0)).put( "usedcount", usedCount ); ((Map)list.get(0)).put( "itemname", itemName ); } /** * DB中没有关联的订单 * @return * * Date :2012-6-7 * Author :GongQiang */ boolean orderNotExist(){ if( list == null || list.isEmpty() ){ return true; } return false; } /** * 该订单已经执行完毕 * @return * * Date :2012-6-7 * Author :GongQiang */ boolean orderFinished(){ BigDecimal remainCountInOrder = (BigDecimal)((Map)list.get(0)).get( "remaincount" ); if( remainCountInOrder != null ){ return remainCountInOrder.compareTo( new BigDecimal("0") ) <= 0 ; } return false; } /** * 检查区分是否正确 * @return * * Date :2012-6-7 * Author :GongQiang */ private boolean checkDistinguish(){ String[] rights = { "保证" }; String dist = (String) ((Map)list.get(0)).get( "chkdistinguish" ); for( int i=0 ; i<rights.length ; i++ ){ if( rights[i].equals( dist ) ){ return true; } } return false; } }
逻辑很简单,这里仅仅测试最基本的4 条执行路径
1、条码解析错误
2、订单在DB中不存在
3、订单已经执行完成
4、分区错误
5、OK
下面是完整的测试类:
package jp.co.snjp.ht.productCheck; import static org.junit.Assert.*; import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import jp.co.snjp.dao.SqlHelper; import org.easymock.classextension.EasyMock; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; public class ProductCheckBarcodeTest { @BeforeClass public static void setUpBeforeClass() throws Exception { } @AfterClass public static void tearDownAfterClass() throws Exception { } /** * 错误条码 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_errorBarcode() throws Exception { ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001" ); assertEquals("error_0", action.doExecute() ); action = new ProductCheckBarcode(); action.setBarcode( "0123456789012345678901234567890123456789555" ); assertEquals("error_0", action.doExecute() ); } /** * DB中没有关联的记录 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_noRecord() throws Exception { SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class ); // 返回结果 EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( new ArrayList<Map<String,Object>>() ); // Replay EasyMock.replay( mockSqlHelper ); ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); assertEquals("error_1", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); //----------------------------------------- mockSqlHelper = EasyMock.createMock( SqlHelper.class ); // 返回结果 EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( null ); // Replay EasyMock.replay( mockSqlHelper ); action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); assertEquals("error_1", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); } /** * 记录已经执行完毕 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_finished() throws Exception { //list -- 返回的结果集 Map<String, Object> map = new HashMap<String,Object>(); map.put("pqty", new BigDecimal("100")); //订单关联数量 map.put("usedcount", new BigDecimal("100")); //已经使用数量 -->剩余数量就是0 map.put("itemname", "TextOrderXXX"); List<Map<String,Object>> list = new ArrayList<Map<String,Object>>(); list.add( map ); SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class ); EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( list ).times(3); // Replay EasyMock.replay( mockSqlHelper ); ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); assertEquals("error_1", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); } /** * 错误的分区 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_errorDistinguish() throws Exception { //list -- 返回的结果集 Map<String, Object> map = new HashMap<String,Object>(); map.put("pqty", new BigDecimal("100")); //订单关联数量 map.put("usedcount", new BigDecimal("50")); //已经使用数量 -->剩余数量就是50 map.put("itemname", "TextOrderXXX"); map.put( "chkdistinguish", "不存在" ); List<Map<String,Object>> list = new ArrayList<Map<String,Object>>(); list.add( map ); SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class ); EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( list ).times(3); // Replay EasyMock.replay( mockSqlHelper ); ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); assertEquals("error_2", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); } /** * 正常 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_ok() throws Exception { //list -- 返回的结果集 Map<String, Object> map = new HashMap<String,Object>(); map.put("pqty", new BigDecimal("100")); //订单关联数量 map.put("usedcount", new BigDecimal("50")); //已经使用数量 -->剩余数量就是50 map.put("itemname", "TextOrderXXX"); map.put( "chkdistinguish", "保证" ); List<Map<String,Object>> list = new ArrayList<Map<String,Object>>(); list.add( map ); SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class ); EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( list ).times(3); // Replay EasyMock.replay( mockSqlHelper ); ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); action.setSession( new HashMap<String,Object>() ); assertEquals("success", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); } }下面详细讲解测试方法的写法:
1、条码解析错误
/** * 错误条码 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_errorBarcode() throws Exception { ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001" ); assertEquals("error_0", action.doExecute() ); action = new ProductCheckBarcode(); action.setBarcode( "0123456789012345678901234567890123456789555" ); assertEquals("error_0", action.doExecute() ); }当条码解析错误时,查询没有机会执行也就没有必要传入 SqlHelper 对象。
2、订单在DB中不存在
/** * DB中没有关联的记录 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_noRecord() throws Exception { SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class ); // 返回结果 EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( new ArrayList<Map<String,Object>>() ); // Replay EasyMock.replay( mockSqlHelper ); ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); assertEquals("error_1", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); //----------------------------------------- mockSqlHelper = EasyMock.createMock( SqlHelper.class ); // 返回结果 EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( null ); // Replay EasyMock.replay( mockSqlHelper ); action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); assertEquals("error_1", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); }为了实现单元测试的隔离性,这里使用了模拟的 SqlHelper 对象。模拟返回一个空的List 或者 null。
注意:模拟方法执行时候是严格的参数匹配的,为简易性这里直接使用 EasyMock.anyObject(),这样任何参数都能匹配执行。
3、订单已经执行完成
/** * 记录已经执行完毕 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_finished() throws Exception { //list -- 返回的结果集 Map<String, Object> map = new HashMap<String,Object>(); map.put("pqty", new BigDecimal("100")); //订单关联数量 map.put("usedcount", new BigDecimal("100")); //已经使用数量 -->剩余数量就是0 map.put("itemname", "TextOrderXXX"); List<Map<String,Object>> list = new ArrayList<Map<String,Object>>(); list.add( map ); SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class ); EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( list ).times(3); // Replay EasyMock.replay( mockSqlHelper ); ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); assertEquals("error_1", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); }在实际代码中,当查询到记录时就要继续两个 SQL查询操作(1、查询订单名称;2、查询订单关联数量和已经检查数量)。并依次往 list 结果集中添加对象,但是在测试中为了方便起见,直接 一次性构造出完整的结果并 重复执行 3次。
4、分区错误
/** * 错误的分区 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_errorDistinguish() throws Exception { //list -- 返回的结果集 Map<String, Object> map = new HashMap<String,Object>(); map.put("pqty", new BigDecimal("100")); //订单关联数量 map.put("usedcount", new BigDecimal("50")); //已经使用数量 -->剩余数量就是50 map.put("itemname", "TextOrderXXX"); map.put( "chkdistinguish", "不存在" ); List<Map<String,Object>> list = new ArrayList<Map<String,Object>>(); list.add( map ); SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class ); EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( list ).times(3); // Replay EasyMock.replay( mockSqlHelper ); ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); assertEquals("error_2", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); }这里就是注意构造参数,使得前面的判断都成功,到这里判断分区时错误。
5、OK
/** * 正常 * * * Date :2012-6-18 * Author :GongQiang * @throws Exception */ @Test public void testDoExecute_ok() throws Exception { //list -- 返回的结果集 Map<String, Object> map = new HashMap<String,Object>(); map.put("pqty", new BigDecimal("100")); //订单关联数量 map.put("usedcount", new BigDecimal("50")); //已经使用数量 -->剩余数量就是50 map.put("itemname", "TextOrderXXX"); map.put( "chkdistinguish", "保证" ); List<Map<String,Object>> list = new ArrayList<Map<String,Object>>(); list.add( map ); SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class ); EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )) .andReturn( list ).times(3); // Replay EasyMock.replay( mockSqlHelper ); ProductCheckBarcode action = new ProductCheckBarcode(); action.setBarcode( "xxx0001|bbb" ); action.setSqlHelper( mockSqlHelper ); action.setSession( new HashMap<String,Object>() ); assertEquals("success", action.doExecute() ); //Verify EasyMock.verify( mockSqlHelper ); }这里要注意,因为实际代码中 调用了sessionMap 的put 方法,因而这里就要传入一个对象。
扩展:当有单独的 Business 层和 DAO 层时候。也许没有办法像 SqlHelper 简单的只需要一个接口方法即可,也许就要每个子 Action 设置相应的Business 对象。