断言
【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
断言的实现就是判断是否符合预期, 不符合就抛异常.
Java 中有提供 assert
(断言)关键字, 但是默认是不生效的, 需要配置虚拟机参数 -enablesystemassertions / -esa
启用断言
JUnit 中有提供此工具类: Assert
其中部分方法:
static public void assertNotNull(Object object)
static public void assertEquals(Object expected, Object actual)
static public void assertTrue(boolean condition)
在这里我安利一个测试框架 AssertJ, 详细的我就不介绍了, 总之 AssertJ 有非常友好的断言 API.
GIVEN - WHEN - THEN
一个标准测试用例的三段式结构:
编写时,我们会精心准备一组输入数据 (Given),然后在调用行为后 (When),断言返回的结果与预期相符 (Then)。
标准原则
【推荐】编写单元测试代码遵守BCDE原则,以保证被测试模块的交付质量。 B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。 C:Correct,正确的输入,并得到预期的结果。 D:Design,与设计文档相结合,来编写单元测试。 E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果。
举个栗子, 对下面这个方法进行测试
public List<Person> handle(Person person) {
if (person.getAge() < 0) {
person.setAge(0);
}
if (person.getBirth().after(new Date())) {
throw new RuntimeException("生日不能晚于当前");
}
return Collections.singletonList(person);
}
边界测试:
@Test
public void testHandle1() {
// GIVEN 年龄小于 0
Person person = new Person(-1, "Sun", new Date());
// WHEN
List<Person> list = handle(person);
// THEN
Assert.assertEquals(list.get(0).getAge(), 0);
Assert.assertEquals(list.get(0).getName(), "Sun");
}
@Test
public void testHandle2() {
// GIVEN: 对象的生日晚于当前时间
Person person = new Person(0, "Sun", new Date(System.currentTimeMillis() + 10000));
RuntimeException exception = null;
try {
// WHEN: 调用被测试方法
List<Person> list = handle(person);
} catch (RuntimeException e) {
exception = e;
}
// THEN: 会抛出异常, 且异常信息为: 生日不能晚于当前
Assert.assertEquals("生日不能晚于当前", exception.getMessage());
}
正确的输入:
@Test
public void testHandle3() {
// GIVEN
Person person = new Person(21, "Sun", new Date(889804800000L));
// WHEN
List<Person> list = handle(person);
// THEN
Assert.assertEquals(list.get(0).getAge(), 21);
Assert.assertEquals(list.get(0).getName(), "Sun");
Assert.assertEquals(list.get(0).getBirth(), new Date(889804800000L));
}
错误的输入:
@Test
public void testHandle3() {
NullPointerException exception = null;
// GIVEN
Person person = null;
try {
// WHEN
List<Person> list = handle(null);
} catch (NullPointerException e) {
exception = e;
}
// THEN
Assert.assertNotNull(exception);
}
对 DAO 层的测试 / 数据库集成测试
【推荐】对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据。 【推荐】和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者对单元测试产生的数据有明确的前后缀标识。
比如要测试一个根据 id 查询用户信息的方法, 不能假设用户是存在的, 直接用库里的 id 去查询, 因为这条数据是不稳定, 你也不知道谁什么时候就把这条数据删了, 这个测试类就跑不通了.
在测试之前要用代码插入一条数据, 测试完成后将数据删除. 如果是 SpringBoot 的测试类的话直接加上 @Transactional
注解就帮你完成了回滚操作.
举个🌰, 测试一个下架商品的方法
@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class GoodsMapperTest {
@Autowired
private GoodsMapper goodsMapper;
/**
* 正确的下架
*/
@Test
public void testOffShelves1() {
// GIVEN: 存入一个上架的商品
GoodsDO goods = new GoodsDO("90012", "鞋子", "42", "上架");
goodsMapper.save(goods);
// WHEN: 执行下架方法
goodsMapper.offShelves(goods.getId());
// THEN: 此商品状态会变成下架
GoodsDO dbGoods = goodsMapper.getById(goods.getId);
Assert.assertEquals(dbGoods.getStatus(), "下架");
}
/**
* 下架商品 id 不存在
*/
@Test
public void testOffShelves2() {
// GIVEN
int id = 123456;
// WHEN
int affected = goodsMapper.offShelves(id);
// THEN
Assert.assertEquals(affected, 0);
}
/**
* 参数为 null
.....
}
对 Service 层的测试
在 Service 层中大多会有对 DAO 层的调用, 这里如果真的调用了 DAO 层, 那就不是单元测试了, 就变成了集成测试, 单元测试类应该不依赖于其他模块的. 在测试 Service 的时候要把对 DAO 层的调用 mock 掉, 这样在 Service 里就专注测试 Service 中的代码, 把 DAO 调用理解成单纯的数据输入就行了.
对 Controller 层的测试
其实 Controller 里基本没有什么代码, 但是 Spring 的执行流程Controller 前面有 Interceptor, 如果仅通过注入 Controller 来测试会漏掉自定义的 Interceptor 和 @Valid 参数验证.
MockMVC 解决了这个问题.
举个🌰 ,保存用户
@Test
public void testAddUser() throws Exception {
User user = new User();
user.setId(1L);
user.setName("jamie");
user.setAge(20);
mockMvc.perform(post("/add")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJSONString(user)))
.andExpect(status().isOk())
.andDo(print())
.andReturn().getResponse().getContentAsString();
}
个人总结
- 写测试类的目的不仅仅是为了自动化测试, 也是为了以后的重构, 重构前测试类能全跑通, 重构后测试类也要全部能跑通, 这也是 TDD 的思想(Red - Green - Refactor).
- 测试类很难维护: 测试类即使写出了花来了, 也不一定会好维护, 稍稍改一下代码, 就需要再维护一下那个方法影响的全部测试用例, 如果真正以 TDD 的方式进行开发也就没有维护测试用例这一说了
- 编写的代码最好能够符合 SOLID 原则, 使测试写起来简单一些