项目文件结构图:
椭圆框中的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 对象。