El Psy Congroo

单元测试规范

基本原则

  • 测试任何可能失败的事物。测试主执行路径很好,而且很需要做;但测试异常处理可能更重要。
  • 测试先行。在写任何代码之前,必须先写一个失败的测试。
  • 为还没有实现的测试代码抛出一个异常。这就避免了该测试通过,而且会提醒你必须实现其代码。
  • 不应该依赖于任何不可控的外部服务,如xingng,使用mock替代
  • 不应该依赖于外部的数据(即不要依赖数据库中现有数据等,以保证数据库变更等不会影响到测试结果),可以在单元测试中自己准备数据
    • 例如班车刷卡:依赖于班车数据,junit中输入某个班车卡号,单数据库中不存在,导致系统报错
    • 这里表现出两点问题:
      • 程序不完善,卡号不存在报null point
      • 依赖于外部已存在的数据,预期数据不存在导致测试失败
  • 测试用基础数据准备
    • 手工制造(简单数据,手工设值或者使用MockTestUtil提供的方法)
    • 文件载入(文本-xml,json;二进制-java对象序列化文件等)
  • 测试动作不应影响持久化数据,即不应该对数据库等造成永久性修改,数据库类操作尽量回滚,对于现有测试用例中的 rollback=false 尽量去除
  • 单元测试需要测试到server层,junit原则上只应用service。
  • 测试优先级
    • 基本的数据库操作方法,测试优先级从高到低依次为 service > manager > dao
    • helper,util等工具类需要单独测试,不能依赖调用方间接的测试

编码规范

  • CRUD测试,基本的方式:建议CRUD每个动作对应一个测试方法,以便隔离各个操作相互间的影响
  • 测试方法名应遵守test_XX1_XX2的命名模式,其中XX1是待测方法名,XX2是测试条件或目的,应当能通过阅读方法名就可以理解要测试的是什么方法,如test_loginByLdap_account_not_exists,test_loginByLdap_pass
  • 一个测试方法应只有一个目的,如测试登录失败和登录成功应分为两个测试
  • 在assert调用中解释失败原因。尽量使用第一个参数是String类型的那个方法,这个参数让你可以提供一个有意义的文本描述,用于在断言失败的时候显示。同样的,fail()调用也应提供文本描述。
  • 选择合适的assert方法使用,而不仅仅是assertTrue
  • 注意assert方法的参数顺序,期望值在前,实际值在后
  • 代码样例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 客户账单未确认 进行批量确认操作
* 前提(客户账单未确认)
* 期望(1、账单确认状态为true 2、账单确认人为当前登录操作员(单元测试下可能取不到,需要mock)3、确认日期为当前时间)
*/
@Test
public void test_batchConfirm_confirmed_not_write_off() {
try {
// 准备客户账单
CusCustomerBillVo billVo = initCusCustomerBill();
PackCusCustomerBillVo packBillVo = cusCustomerBillService.createCusCustomerBill(billVo);
// 进行批量确认操作
PackCusCustomerBillVo writeOffBill = cusCustomerBillService.batchConfirm(Arrays.asList(packBillVo.getVo()), "批量确认测试");
// 校验数据
assertEquals("账单确认状态", Boolean.TRUE, writeOffBill.getVoList().get(0).getIsConfirmed());
assertNotSame("账单确认人", billVo.getConfirmedManName(), writeOffBill.getVoList().get(0).getConfirmedManName());
assertNotSame("确认日期",billVo.getConfirmedTime(), writeOffBill.getVoList().get(0).getConfirmedTime());
} catch (CustomerBizException e) {
BizExceptionAssert.fail(e);
}
}

FAQ

  • 预期抛出异常使用 @Test(except=xxException.class) 方式
  • DAO CRUD 测试组织问题
  • 一条数据字段被多次修改导致测试失败
  • 外键约束的处理
    • 使用现有数据
    • 准备所有相关约束数据
  • 测试环境、数据的统一准备
    • 可以使用@Before修饰的方法,在每个测试方法调用前会自动执行
  • 唯一约束冲突问题
    • 使用 MockTestUtil 的 getJavaBean 方法生成的对象属性值可能会与数据库中现有记录相同,导致写入数据库失败,解决办法是 mock 出对象后针对该属性进行手工设值
  • 测试用例编写粒度
    • 基本的 DAO 级别 CRUD 测试可以少写一点
  • 测试方法间的依赖性问题
    • 测试方法间不应有先后顺序依赖
  • 复杂方法的测试(逻辑较复杂、边界条件较多等)
    • 可以分成多个测试用例,每个测试用例测试一个逻辑分支或编辑条件
  • 数据写入MQ和数据库的ID不一致问题
  • 数据库字段是char(2),java 中是String 执行testCRUD方法时,报错
    • 解决方法:修改mockBeanFactory类中对应的方法,调整特殊字段的属性
  • 无意义对象创建存在问题时,比如有外键约束,数据有效性判断等
  • 可以 VehicleSiteManageVo vo = MockTestUtil. getJavaBean(VehicleSiteManageVo. class ); 构建对象,再针对特定数据构造
  • 考虑数据查询量的大小,以及单元测试的运行时间
  • 分区表写入问题
    • 有时候使用 MockTestUtil 的 getJavaBean 方法生成的 PO 对象写入数据库时出现 DAOAccessException,错误编号为ORA-14400,这种错误是可能是因为对应表依据某个或某几个字段值做了表分区,mock 出来的对象该属性值超出了表分区所允许的值范围,从而出现这个错误。解决办法是找到对应表并查看建表 sql,按照该表的表分区定义将相应 PO 属性设置成表分区所允许的值。

关于测试基类

  • AbstractServiceTest
    • 一般的测试类请继承这个基类,不带xingng等外部依赖,如果业务实现中增加了新的xingng接口调用,需要到com.best.oasis.express.test.stub这个包下实现一个相应的stub类来模拟
  • AbstractIntegrationServiceTest
    • 带完整的外部依赖,需要真实调用xingng等外部系统的测试类才会继承这个基类,请尽量避免