单元测试之道

开发设计

抽象产品和需求

1、站在用户角度思考产品的行为
2、分析业务领域模型
3、测试领域模型

一切面向接口设计

1、设计基于restful的风格接口:

  1. uri代表了资源的实体
  2. http method描述了资源的状态转化,目前有四个:get,post,put,delete
  3. 例如,获取用户信息,get /user/info or /user/1

2、设计业务方法接口

  1. 每个业务方法应该都能描述用户的操作
  2. 基于最简单的业务方法进行组合或扩展
  3. 测试每一个或每一组方法

3、设计功能性接口

  1. 单一原则
  2. 依赖倒置

单元测试

了解junit

  1. 使用assert断言,验证程序输出的结果是否正确
  2. 使用@Before初始化测试数据
  3. 使用@After清除测试后的数据
  4. 使用@Test注明测试单元
  5. 使用@Test中的expected测试异常状态单元

编写可测试的类

  1. 在src/main/test中创建单元测试
  2. 测试的package需要和被测试的类的package保持一致
  3. 对于需要测试的Function或者类,尽可能使用默认的修饰符,避免使用private修饰符
  4. 如果Function使用了外部类,使用mock方式处理外部类的方法返回数据
  5. 基于单一原则,设计类以及函数
  6. 基于依赖倒置原则,设计和抽象对象和函数

了解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插件生成覆盖率报告,每次发布项目之前应该急需要生成一份报告出来

猜你喜欢

转载自blog.csdn.net/guzhangyu12345/article/details/73104853