什么是单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。类比到盖楼就是对每块砖头,每个钢筋,每份水泥做检查,只有每一块砖头都满足要求,最后搭建起来的房子才可能没问题。
单元测试和集成测试的区别在于,单元测试是检查每个组件方法是否无误,集成测试在于将组件彼此依赖组装后进行验证。
单元测试的优点/作用
1.修改后的代码可以做快速测试,避免修改后,只能祈祷其正确。
2.可以倒逼业务代码的设计松耦合,满足设计原则,更清晰,易懂。
3.单元测试的测试用例本身也是对测试目标的一种解释说明。
好的单元测试的特点
1.运行快
2.可以帮助定位问题,具有表达性,表达出这个测试是测什么
3.应该是自动化的
4.每个单元测试不应该依赖其它测试的结果和执行顺序,单元测试框架可以按任意的顺序执行每个测试
5.每个单元测试不应该依赖数据库,外部文件,外部接口,或者任何长时间运行或不稳定的任务。
6.测试代码应该跟生产代码拥有同等标准要求
不好的单元测试的特点
1.与数据库有交互
2.与网络交互
3.与文件系统交互
4.需特定环境才可运行,例如必须先修改配置文件
Mock技术
为了满足单元测试的隔离性,隔离外部环境,我们需要将外部依赖,通过替身的方式,模拟出一个可以满足我们测试的假对象,这个对象我们可以定义外部依赖类方法的本次测试的返回值,来控制其行为,进而实现隔离。 常用的mock框架有Mockito,PowerMock。或者直接自行实现接口。
编写好的单元测试
为编写出好的单元测试,首先被测代码应具备可测试性。当写出具有可测试性的代码时,其实其变得遵从了经典的设计原则和思想,可读性,可维护性,也会提高。
下面列举出什么样的代码不可测试,通过对比,即可学习到如何编写具有可测试性的代码。
1.直接在方法体内 new 对象。
反例:
public boolean queryAndCheckUserInfo(String id) {
UserJdbcMapper userJdbcMapper = new UserJdbcMapperImpl();
UserInfo user = userJdbcMapper.queryUserInfo(id);
return user.getAge() > 15;
}
直接通过new构造出UserJdbcMapper 接口对应的实现类跟数据库交互的类,该类并非构造时传入或注入依赖,该类无法被mock替换,所以无法编写隔离数据库/外部依赖的单元测试。
正例1,从外部传入:
public boolean queryAndCheckUserInfo(String id, UserJdbcMapper userJdbcMapper ) {
UserInfo user = userJdbcMapper.queryUserInfo(id);
return user.getAge() > 15;
}
上述代码,可以用mock框架,也可以自行实现一个UserJdbcMapper 接口的实现类传入该方法。
单元测试:
@Test
public void test_queryAndCheckUserInfo() {
UserJdbcMapper userJdbcMapper = new UserJdbcMapperImpl();
// 使用mock 控制依赖的其他类的方法行为,进而屏蔽与数据库交互
Mockito.when(orderMapper.queryUserInfo(1)).thenReturn(new UserInfo(“张三”,
17));
boolean ans = userService.queryAndCheckUserInfo(1, userJdbcMapper );
Assert.assertEquals(ans, true);
}
正例2,依赖注入
@Service
public class UserService {
@Autowired
private UserJdbcMapper userJdbcMapper;
public boolean queryAndCheckUserInfo(String id) {
UserInfo user = userJdbcMapper.queryUserInfo(id);
return user.getAge() > 15;
}
}
可以使用Mockito框架编写单元测试如下:
@RunWith(MockitoJUnitRunner.class)
public class UserServiceMockTest {
@InjectMocks
private UserService userService;
@Mock
private UserJdbcMapper orderMapper
@Test
public void test_queryAndCheckUserInfo() {
// 使用mock 控制依赖的其他类的方法行为,进而屏蔽与数据库交互
Mockito.when(orderMapper.queryUserInfo(1)).thenReturn(new UserInfo(“张三”, 17));
boolean ans = userService.queryAndCheckUserInfo(1);
Assert.assertEquals(ans, true);
}
}
通过上述修改,除了具备可测试性,而且还满足了依赖倒置原则。
2.未决行为
代码可能是随机或不确定的,例如跟时间,随机数有关的代码。方法体内尽量不要出现随机,如果一定要出现,最好调用时传入或额外封装一个方法传入,保证实现的业务代码可被测即可,额外封装的单独方法非常简单可不进行单元测试。
反例:
public boolean checkDate(Date needCheckDate) {
long currentTime = System.currentTimeMillis();
return currentTime > needCheckDate.getTime();
}
跟系统时间耦合,倒置单元测试编写后可能随着时间而变化结果。
正例:
public boolean checkDate(Date needCheckDate, Date indexDate) {
return indexDate.getTime() > needCheckDate.getTime();
}
public boolean checkDateNow(Date needCheckDate) {
return checkDate(needCheckDate, new Date());
}
一定要校验当前时间的话,可以单独抽出一个方法checkDateNow,该方法因简单可认为其本身无问题,可不进行单元测试,只需测试 checkDate 方法即可。 随机数也是同样道理。
单元测试:
@Test
public void testCheckDate() {
Date needCheckDate = new SimpleDateFormat(“yyyy-MM-dd
HH:mm:ss”).parse(“2022-11-01 16:01:02”);
Date indexDate= new SimpleDateFormat(“yyyy-MM-dd
HH:mm:ss”).parse(“2022-11-02 16:02:03”);
Assert.assertTrue(checkDate(needCheckDate , indexDate));
}
3.全局变量
因为好的单元测试,每个单元测试都互不影响,可并发执行,如果被测代码出现全局变量,则可能导致因运行顺序前后不同,导致结果不定。
如果被测方法涉及全局变量,也是可以例如上述未决条件一样,抽出不依赖全局变量的方法,单独测试,依赖全局变量的方法仅用于传值,可不做测试。
4.复杂继承
若父类需要 mock 某个依赖对象才能进行单元测试,那所有的子孙类,在编写单元测试的时候,都要 mock 这个依赖对象。越底层的子类,要mock的越多,还需要去看父类实现,导致测试困难。继承体系不能过深,本身也是高质量代码编程原则,所以通过代码的可测试性,也可以提升业务代码编写的质量。
5.高耦合代码
一个类,依赖了几十个其他类对象,才可完成工作,代码耦合度极高,在编写单元测试时,需要mock几十个依赖对象。对于编写单元测试代码,也是十分困难。从程序设计角度来看,也不应该如此设计一个类。
总结
好的单元测试,彼此独立,运行快速,不受时间空间配置文件等外部依赖条件影响。通过Mock技术,可以实现对外部依赖的解耦。当发现很难编写单元测试时,记得检查一下是否是代码本身就不具备可测试性。通过上面的例子,也可发现,具备可测试性的本质,关键就是解耦。