认识单元测试
什么是单元测试
测试有黑盒测试和白盒测试之分,黑盒测试顾名思义就是我们不了解盒子的内部结构,我们通过文档或者对该功能的理解,指定了相应的输入参数,然后判断得出的结果是否正确。普通的用户、
开发人员、QA工程师都可以进行黑盒测试。
与之对应的白盒测试则需要了解到内部的实现细节,一般是由开发人员自己来进行的,是基于对代码逻辑结构、各个关联方法了解上进行的。
白盒测试主要有2种类型
- 静态代码分析:静态的分析代码质量,如用Findbugs插件扫描代码。
- 动态测试:单元测试,用预先的一组数据动态的调用代码。
单元测试的必要性
在金字塔越下面的地方 bug 是最多、最好发现、修改成本最小的。反之逐渐往上 bug 的修复成本是越来越大的。直观的感受就是一个开发期的 bug,看一眼代码可能就知道问题了。如果是换成线上的,好一点的话通过日志可以判断出来,如果日志看了没发现什么那就有点惨了。有时候需要在测试环境模拟生产的场景进行排查等。
单元测试具有以下几个好处
- 方便对局部代码逻辑进行验证(如:微服务架构的话 rpc 端的方法不好触发,不用启动整个系统)
- 快速的反馈问题(将问题快速的扼杀在构建前)
- 给代码重构提供了底气(复杂的模块经常不敢轻易变动,因为关联太多怕导致其它异常,有时候也经常发生在修补了一个bug引发了另一个bug的问题)
单元测试的基础知识
如何编写单元测试
可理解的代码非常重要,测试代码也是如此。建议测试代码遵循 BDD(Behavior Driven Development )的风格,即使代码再复杂你也要将它抽象出 //given //when //then 三个基础部分。
通过测试用例反思你的代码
- given 定义了一大堆,就是在测试一个方法的时候需要 mock 一大堆对象,这时候可能是代码设计有误,内聚力不足、与外部高度耦合、职责模糊。
- when 一般只有一个,如果需要调用多个的话,可能实现代码没有给使用者提供合适的接口,需要调用一大堆接口才能完成一个动作。
- then 主要是要去校验结果,这边的校验要尽可能的包含所有出参结果的校验。不能只看调用后没报错,或者返回的状态码是对的就好了。
import static org.mockito.BDDMockito.*;
Seller seller = mock(Seller.class);
Shop shop = new Shop(seller);
public void shouldBuyBread() throws Exception {
//given
given(seller.askForBread()).willReturn(new Bread());
//when
Goods goods = shop.buyBread();
//then
assertThat(goods, containBread());
}
拓展:
- TDD(测试驱动开发)这是极限编程中推崇的开发模式,要求在开发前先写测试代码,基于测试的代码来编写业务代码,这样的好处一方面可以反复检验你的功能代码,一方面有助于你业务代码具有高内聚低耦合的效果。
- BDD(行为驱动开发)可以看作 TDD 的一个补充,它主要是输出一些更贴近用户的文档,让用户更好理解。 据我简单的理解是:基于BDD 的测试用例你要充分写好你的注解,given(说明好上下文),when(指定哪个事件),then(对结果进行较详细的说明)
- DDD(领域驱动开发)他主要是对业务进行建模,是业务人员与开发人员沟通的桥梁,他要求开发人员在写代码前要充分的理解业务。现在微服务的架构很火热,微服务里面注重业务的划分,DDD 是微服务的一个不错的实践方式。
基础设施
单元测试一般可以借助自动化构建工具,每次打包的时候进行自动化测试,有问题直接抛出,保证了打包程序的质量,单元测试通过后可以自动发布到开发、QA、仿真等环境。 常见的自动化构建工具有:Jenkins、gitlab-ci、gitlab、docker
常见术语
- stub
很多人把它叫做桩。 测试某个代码时可能需要访问外部系统的服务,这时候我们可以启一个模拟的服务来代替。(比如系统中需要对接各个快递公司的服务)。
- mock
与stub类似,都是用来模拟一些外部依赖。stub主要指的是外部系统,mock一般是本系统内的其他方法。
- spy
spy是间谍的意思,监视某个对象的行为。mock是模拟了整个对象,而spy的对象还是真实的对象,它可以对真实对象中的某些行为进行改变。
@Test
public void real_partial_mock(){
List list = spy(new ArrayList());
// 执行的是list的真实api
assertEquals(0,list.size());
A a = mock(A.class);
// 执行的所有方法都不是A的
when(a.doSomething(anyInt())).then(。。。);
}
单元测试实战
工具介绍
mocktio
单元测试中一个很好的测试工具,简单列举下它的功能点,详情点击:传送门
- 可以mock对象 @Mock
- 可以spy某个真实对象的个别方法 @spy
- 可以方便的往某个对象中注入mock对象 @InjectMocks
- 可以模拟调用方法时的参数匹配 when(mockedList.get(anyInt())).thenReturn("element");
- 可以判断某个方法调用次数 verify(mockedList, times(1)).add("once");
- 可以模拟方法调用抛出异常 when(mockedList.get(1)).thenThrow(new RuntimeException());
powermock/jmock
可对静态方法、私有函数、Final函数进行模拟
testNG
可以比较方便的进行集成测试
AssertJ
比junit强大很多的断言工具,能够很强大的进行流式断言。
传送门
Assertions.assertThat(propsSendDetailPoList)
.isNotEmpty()
.hasSize(3)
.extracting(PropsSendDetailPo::getFcalqty1)
.contains(0L,0L,2L);
场景分析
静态对象mock
参看
@RunWith(PowerMockRunner.class) //1
@PrepareForTest({WebChatUtil.class}) //2
public class WebChatUtilTestCase extends AbstractJUnit {
@Before
public void init(){
PowerMockito.mockStatic(WebChatUtil.class);// 3
}
@Test
public void testWebchatEnable(){
try {
Calendar c = Calendar.getInstance();
c.set(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DATE), 16, 35);
PowerMockito.spy(WebChatUtil.class); // 创建spy,如果不创建的话,后面调用WebChatUtil就都是Mock类,这里创建了spy后,只有设置了mock的方法才会调用mock行为
PowerMockito.doReturn(c).when(WebChatUtil.class, "getCurrentTime"); //Mock私有方法
} catch (Exception e) {
e.printStackTrace();
}
}
}
跨类mock
你要测试的方法里面引用了别的对象的方法,对于这个待测方法里面依赖别的对象你获取不到,如何对他进行mock。
- 最原始的方法-自己用反射实现 此时要测试 A 对象,A里面引用了 B 对象,用来进行持久化操作的,这时候可以通过反射,将 A 对象的 B 属性替换成自己定义的mock对象。 B bmapper = mock(B.class); // 一个待测试的对象 A a = new A(); // bPoMapper 是 A 里面的一个属性(处理持久化用的) Class<A> aclass = A.class; Field bMapperField = aclass.getDeclaredField("bmapper"); bMapperField.setAccessible(true); bMapperField.set(a, bmapper);
- 借助mocktio工具 //@InjectMocks 会将所有 @mock对象注入到自己的属性中 @InjectMocks A a; @Mock B b;
常见问题
- 很多人会在每个类中继承 springjunit 框架导致构建时候非常慢,每继承一个spring容器就会反复启动一次,严重占用了构建机器的性能也影响效率,我们只是为了测试代码逻辑没什么必要启动spring容器,依赖的 bean 可以手动 new 出来,可以自己 mock 相应对象。有的人启动 spring 容器是为了能够连接数据库可以写一个连接数据库的基类可以借助某些工具 dbunit 等
- 测试数据依赖某个数据库测试对于的数据全部要自己初始化,测试用例跑完所有的数据必须回滚。可以用内存数据库可以在构建的时候专门指定一个测试数据库
- 不要只是为了覆盖率而测试要对测试的场景足够清楚,对返回对断言足够细致,不然很难发现问题,就会导致一个现象 - 你的测试用例只是为了证明你对代码是对的,而不是在校验你对代码。