项目文件结构图:
椭圆框中的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 订单区分错误*/@Overridepublic 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 {@BeforeClasspublic static void setUpBeforeClass() throws Exception {}@AfterClasspublic static void tearDownAfterClass() throws Exception {}/*** 错误条码* ** Date :2012-6-18* Author :GongQiang* @throws Exception */@Testpublic 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 */@Testpublic void testDoExecute_noRecord() throws Exception {SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class );// 返回结果EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )).andReturn( new ArrayList<Map<String,Object>>() );// ReplayEasyMock.replay( mockSqlHelper );ProductCheckBarcode action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );assertEquals("error_1", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );//-----------------------------------------mockSqlHelper = EasyMock.createMock( SqlHelper.class );// 返回结果EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )).andReturn( null );// ReplayEasyMock.replay( mockSqlHelper );action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );assertEquals("error_1", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );}/*** 记录已经执行完毕* ** Date :2012-6-18* Author :GongQiang* @throws Exception */@Testpublic 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")); //已经使用数量 -->剩余数量就是0map.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);// ReplayEasyMock.replay( mockSqlHelper );ProductCheckBarcode action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );assertEquals("error_1", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );}/*** 错误的分区* ** Date :2012-6-18* Author :GongQiang* @throws Exception */@Testpublic 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")); //已经使用数量 -->剩余数量就是50map.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);// ReplayEasyMock.replay( mockSqlHelper );ProductCheckBarcode action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );assertEquals("error_2", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );}/*** 正常* ** Date :2012-6-18* Author :GongQiang* @throws Exception */@Testpublic 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")); //已经使用数量 -->剩余数量就是50map.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);// ReplayEasyMock.replay( mockSqlHelper );ProductCheckBarcode action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );action.setSession( new HashMap<String,Object>() );assertEquals("success", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );}
}
下面详细讲解测试方法的写法:
1、条码解析错误
/*** 错误条码* ** Date :2012-6-18* Author :GongQiang* @throws Exception */@Testpublic 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 */@Testpublic void testDoExecute_noRecord() throws Exception {SqlHelper mockSqlHelper = EasyMock.createMock( SqlHelper.class );// 返回结果EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )).andReturn( new ArrayList<Map<String,Object>>() );// ReplayEasyMock.replay( mockSqlHelper );ProductCheckBarcode action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );assertEquals("error_1", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );//-----------------------------------------mockSqlHelper = EasyMock.createMock( SqlHelper.class );// 返回结果EasyMock.expect( mockSqlHelper.executeQuery( (String)EasyMock.anyObject() )).andReturn( null );// ReplayEasyMock.replay( mockSqlHelper );action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );assertEquals("error_1", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );}
为了实现单元测试的隔离性,这里使用了模拟的 SqlHelper 对象。模拟返回一个空的List 或者 null。
注意:模拟方法执行时候是严格的参数匹配的,为简易性这里直接使用 EasyMock.anyObject(),这样任何参数都能匹配执行。
3、订单已经执行完成
/*** 记录已经执行完毕* ** Date :2012-6-18* Author :GongQiang* @throws Exception */@Testpublic 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")); //已经使用数量 -->剩余数量就是0map.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);// ReplayEasyMock.replay( mockSqlHelper );ProductCheckBarcode action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );assertEquals("error_1", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );}
在实际代码中,当查询到记录时就要继续两个 SQL查询操作(1、查询订单名称;2、查询订单关联数量和已经检查数量)。并依次往 list 结果集中添加对象,但是在测试中为了方便起见,直接
一次性构造出完整的结果并
重复执行 3次。
4、分区错误
/*** 错误的分区* ** Date :2012-6-18* Author :GongQiang* @throws Exception */@Testpublic 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")); //已经使用数量 -->剩余数量就是50map.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);// ReplayEasyMock.replay( mockSqlHelper );ProductCheckBarcode action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );assertEquals("error_2", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );}
这里就是注意构造参数,使得前面的判断都成功,到这里判断分区时错误。
5、OK
/*** 正常* ** Date :2012-6-18* Author :GongQiang* @throws Exception */@Testpublic 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")); //已经使用数量 -->剩余数量就是50map.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);// ReplayEasyMock.replay( mockSqlHelper );ProductCheckBarcode action = new ProductCheckBarcode();action.setBarcode( "xxx0001|bbb" );action.setSqlHelper( mockSqlHelper );action.setSession( new HashMap<String,Object>() );assertEquals("success", action.doExecute() );//VerifyEasyMock.verify( mockSqlHelper );}
这里要注意,因为实际代码中 调用了sessionMap 的put 方法,因而这里就要传入一个对象。
————————————————————————————————————
扩展:当有单独的 Business 层和 DAO 层时候。也许没有办法像 SqlHelper 简单的只需要一个接口方法即可,也许就要每个子 Action 设置相应的Business 对象。