开发设计
抽象产品和需求
1、站在用户角度思考产品的行为
2、分析业务领域模型
3、测试领域模型
一切面向接口设计
1、设计基于restful的风格接口:
- uri代表了资源的实体
- http method描述了资源的状态转化,目前有四个:get,post,put,delete
- 例如,获取用户信息,get /user/info or /user/1
2、设计业务方法接口
- 每个业务方法应该都能描述用户的操作
- 基于最简单的业务方法进行组合或扩展
- 测试每一个或每一组方法
3、设计功能性接口
- 单一原则
- 依赖倒置
单元测试
了解junit
- 使用assert断言,验证程序输出的结果是否正确
- 使用@Before初始化测试数据
- 使用@After清除测试后的数据
- 使用@Test注明测试单元
- 使用@Test中的expected测试异常状态单元
编写可测试的类
- 在src/main/test中创建单元测试
- 测试的package需要和被测试的类的package保持一致
- 对于需要测试的Function或者类,尽可能使用默认的修饰符,避免使用private修饰符
- 如果Function使用了外部类,使用mock方式处理外部类的方法返回数据
- 基于单一原则,设计类以及函数
- 基于依赖倒置原则,设计和抽象对象和函数
了解mockito
方法介绍
1、使用mock函数模拟一个外部类的实现
2、使用spy函数监控一个外部类的调用
3、使用verify验证被spy过的外部类的调用次数
4、mock后的对象可以使用
when..thenDo..something的表达式处理函数内的调用逻辑
测试ibatis的dao
1、初始化dataSource,可以实现一个测试工具类,注入相关的数据库连接的配置,然后实例化出来
2、然后初始化SqlMapClientFactoryBean,只需要将相关的xml配置设置到mappingLocations属性,然后调用afterPropertiesSet初始化SqlMapClient,最后就可以直接调用getObjectClient
3、dao一般都继承于SqlMapClientDaoSupport,例如UserDao,那么基于前面的DataSource和SqlMapClient,就可以直接使用new UserDao(dataSource,sqlMapClient)构建出来
4、通过使用@Before和@After,构建dao和销毁dao
测试业务层的函数
1、如果业务层的函数只是一些逻辑处理,那么可以直接使用junit的assert进行验证
2、如果函数依赖了dao层,那么使用mock模拟成dao处理
3、如果函数依赖了其他service服务,那么同样使用mock进行模拟处理
示例如下:
public class BaseOrderMinusLockBusinessTest {
OrderAddInStockBusiness orderAddInStockBusiness=new OrderAddInStockBusiness();
Staff staff;
Order order;
@Before
public void setUp(){
staff=new Staff();
order=new Order();
orderAddInStockBusiness.goodsSectionOrderRecordDao= Mockito.mock(GoodsSectionOrderRecordDao.class);
orderAddInStockBusiness.wmsInventoryUpdateBusiness=Mockito.mock(WmsInventoryUpdateBusiness.class);
Mockito.doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
List<Long> orderIds=(List<Long>)invocationOnMock.getArguments()[1];
List<GoodsSectionOrderRecord> list=new ArrayList<GoodsSectionOrderRecord>();
for(Long orderId:orderIds){
GoodsSectionOrderRecord gor=new GoodsSectionOrderRecord();
gor.setOrderId(orderId);
gor.setGetNum(1);
list.add(gor);
GoodsSectionOrderRecord gor1=new GoodsSectionOrderRecord();
gor1.setOrderId(orderId);
gor1.setGetNum(2);
list.add(gor1);
}
return list;
}
}).when(orderAddInStockBusiness.goodsSectionOrderRecordDao).findByOrderIds(Mockito.any(Staff.class), Mockito.anyListOf(Long.class), Mockito.anyLong());
}
@Test
public void testOp0(){
orderAddInStockBusiness.op0(staff,order);
Mockito.doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
List<WmsChangeAffect> wmsChanges=(List<WmsChangeAffect>)invocationOnMock.getArguments()[0];
Asserts.check(wmsChanges.size()==2,"需要更新的货位库存数有误!");
return null;
}
}).when(orderAddInStockBusiness.wmsInventoryUpdateBusiness)
.updateAssoBatch(Mockito.anyListOf(WmsChangeAffect.class), Mockito.any(Staff.class));
}
}
测试控制器层的函数
1、遵循单一原则,controller和service实现一对一的映射
2、controller是属于state-ful的类,所以只需要将service mock处理,并验证传递的参数是否被service成功调用
测试领域模型
1、由于业务使用的贫血模型,所以领域模型分为model和service
2、model的get、set方法有些可能也包含了业务逻辑,所以对get和set方法也需要进行单元测试
测试多线程的函数
1、一般用于测试多线程环境下某个业务场景是否能保障线程安全,例如开启100个线程,同时对一个共享变量计数,同时对一个共享变量计数10次,那么最终期望的结果为1000
2、测试性能,验证最终期望的消费时间是否合理
3、测试多线程组件是否正确,例如阻塞队列的函数、计划任务、并发聚合函数等
示例如下:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/spring-*.xml")
public class TestStockService {
@Resource
IStockService stockService;
Staff staff;
Long companyId = 10001L;
User user;
Company company;
Long sysItemId=1l;
Long sysSkuId=3l;
Long warehouseId=4l;
ExecutorService executor= Executors.newFixedThreadPool(10);
@Resource
JdbcTemplate jdbcTemplate;
@Before
public void setUp() throws Exception {
company = new Company();
company.setId(companyId);
CompanyProfile p = new CompanyProfile();
p.setCompanyId(company.getId());
p.setDbInfo(new DbInfo());
company.setProfile(p);
staff = new Staff();
staff.setCompanyId(company.getId());
staff.setCompany(company);
staff.setId(10001L);
Map<Long, User> userMap = new HashMap<Long, User>();
user = new User();
user.setTaobaoId(10001L);
user.setId(10L);
userMap.put(user.getId(), user);
staff.setUserIdMap(userMap);
initData();
}
private void initData() {
jdbcTemplate.update(" delete from stock_0 where company_id=? ",companyId);
jdbcTemplate.update(" delete from stock_order_record_0 where company_id=? ",companyId);
jdbcTemplate.update("insert into stock_0(company_id,sys_item_id,sys_sku_id,available_in_stock,lock_stock,defective_stock,ware_house_id) " +
" values(?,?,?,?,?,?,?)",companyId,sysItemId,sysSkuId,-100,100,100,warehouseId);
for(int i=0;i<100;i++){
jdbcTemplate.update("insert into stock_order_record_0(stock_status,lock_type,sid,order_id,sys_item_id,sys_sku_id,num,warehouse_id,company_id,stock_num,difference_value)" +
" values(?,?,?,?,?,?,?,?,?,?,?) ",3,0,i,i,sysItemId,sysSkuId,1,warehouseId,companyId,0,1);
}
jdbcTemplate.update("insert into stock_0(company_id,sys_item_id,sys_sku_id,available_in_stock,lock_stock,defective_stock,ware_house_id) " +
" values(?,?,?,?,?,?,?)",companyId,sysItemId,sysSkuId+1,-100,100,100,warehouseId);
for(int i=0;i<100;i++){
jdbcTemplate.update("insert into stock_order_record_0(stock_status,lock_type,sid,order_id,sys_item_id,sys_sku_id,num,warehouse_id,company_id,stock_num,difference_value)" +
" values(?,?,?,?,?,?,?,?,?,?,?) ",3,0,i,i,sysItemId,sysSkuId+1,1,warehouseId,companyId,0,1);
}
}
@Test
public void save4StockInventory(){
final CountDownLatch latch=new CountDownLatch(1);
for(int i=0;i<10;i++){
final StockChangeAffect stockChangeAffect=new StockChangeAffect();
stockChangeAffect.setSysItemId(sysItemId);
stockChangeAffect.setSysSkuId(sysSkuId);
stockChangeAffect.setWareHouseId(warehouseId);
stockChangeAffect.setAvailableStockWithLock((i+1)*10l);
stockChangeAffect.setFromWms(false);
executor.execute(new Runnable() {
public void run() {
try {
latch.await();
stockService.save4StockInventory(staff,stockChangeAffect);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
latch.countDown();
while(!executor.isTerminated()){
try {
Thread.sleep(1000l);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
基于业务的边界分析
1、分析领域模型的状态转变,例如订单状态由待付款变为待发货的case,或者库存状态由充足变为缺货的case等
2、分析行为实施到不同的数据状态时,验证测试所期望的结果,例如:用户发货,如果对待发货的、待付款的、已发货的、异常状态的订单进行处理等不同的case
3、分析依赖模块的业务边界数据如何影响到自己的业务,例如订单向库存系统锁定库存,此时库存系统会返回各种库存状态,对于不同的库存状态需要编写不同的case进行校验
基于工具类的边界分析
1、空指针异常
2、数组越界异常
3、工具函数返回值是否是自己所期望的
4、最小值和最大值的测试
单元测试命名规范
1、一个单元测试类,在junit中称之为测试套件,一个套件中包含多个单元测试,那么套件的命名一般以Test开头,例如测试的类为UserDao,那么单元测试的套件名称为TestUserDao
2、标记为@Before的方法名称为setUp
3、标记为@After的方法名称为tearDown
4、标记为@Test的方法名称一般为test+测试的方法名称,例如测试UserDao.queryById,那么测试方法名为testQueryById
单元测试注意事项
1、一个单元测试的套件应该是针对某一个类,不要混合其他类的测试
2、一个单元测试应该尽可能地测试当前函数的逻辑,如果函数依赖了其他类或组件,应尽可能通过mock方式避开
3、单元测试的结构尽可能简单
4、每当重构一个类或函数时,应当要运行整个模块的单元测试
5、不要滥用Mockito工具,在必须要使用的时候才去使用它
6、每个单元测试应该尽可能简单,最好是一个case对应一种数据或者一个数据边界等
单元测试中使用到的代码不要设计得太复杂,可以通过复制的方式构建,然后进行一个测试数据的修改
测试覆盖率
测试覆盖率的查看能够有效了解到整个项目被测试的代码的概况
可以使用Cobertura的maven插件生成覆盖率报告,每次发布项目之前应该急需要生成一份报告出来