单元测试
软件测试按照阶段可分为单元测试、集成测试、系统测试以及验收测试,今天我们要介绍的就是单元测试。
阶段 | 测试对象 | 测试人员 | 测试方法 | |
---|---|---|---|---|
单元测试 | 编码后 | 最小单位程序模块 | 软件开发人员 | 白盒测试 |
集成测试 | 单元测试之后 | 组装后的模块 | 软件开发人员 | 灰盒测试 |
系统测试 | 集成测试之后 | 已经集成好的软件系统 | 测试人员 | 黑盒测试 |
验收测试 | 系统测试之后 | 整个系统 | 测试人员 | 黑盒测试 |
1、什么是单元测试?
首先我们要先了解一下什么是单元,单元就是指人为规定的最小的被测功能模块,在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。而单元测试则是对这些 ” 单元 “ 进行检测,看功能是否正确。单元测试的编写人员是软件开发人员。
2、单元测试的作用和局限性有哪些呢?
从测试金字塔中我们可以发现,越是底层的测试,牵扯到相关内容越少,而高层测试则涉及面更广,因此bug发现的越晚,修改的成本就越高。比如单元测试,它的关注点只有一个单元,而没有其它任何东西。所以单元测试修改bug的成本是最小的。进行单元测试具有一下作用:
单元测试的局限性:
- 单元测试只测试程序单元自身的功能。因此,它不能发现集成错误、性能问题、或者其他系统级别的问题。
3、好的单元测试时什么样的?
-
正确清晰:有利于帮助其他开发者理解代码逻辑,理解如何使⽤相关的类或者函数。
-
完整:良好设计的单元测试案例覆盖程序单元分支和循环条件的所有路径。好的单元测试完备⽽不重复。同样的测试场景,或者同类型的测试输⼊不要写多个单元测试,找⼀个有代表性的场景输⼊就可以了。
-
健壮:当被测试的类或者函数被修改内部实现或者添加功能时,⼀个好的单测应该完全不需要被修改或者只有极少的修改。
4、单元测试的步骤
单元测试的代码结构⼀般一个三步经典结构:准备,调⽤,断⾔。
-
准备:⽬的是准备好调⽤所需要的外部环境,如数据,Stub,Mock,临时变量,调⽤请求,环境背景变量等等。
-
调⽤:实际调⽤需要测试⽅法,函数或者流程。
-
断⾔:判断调⽤部分的返回结果是否符合预期。
测试框架JUnit
JUnit是用于编写可复用测试集的简单框架,是xUnit的一个子集。xUnit是一套基于测试驱动开发的测试框架,有PythonUnit、CppUnit、JUnit等。
1、常用注解的使用
注解 | 说明 |
---|---|
@Before | 初始化方法 |
@After | 释放资源 |
@Test | 测试方法,在这里可以测试期望异常和超时时间 |
@Ignore | 忽略的测试方法 |
@BeforeClass | 针对所有测试,只执行一次,且必须为static void |
@AfterClass | 针对所有测试,只执行一次,且必须为static void |
@RunWith | 指定测试类使用某个运行器 |
@Parameters | 指定测试类的测试数据集合 |
@Rule | 允许灵活添加或重新定义测试类中的每个测试方法的行为 |
@FixMethodOrder | 指定测试方法的执行顺序 |
public class ClassNameTest {
@BeforeClass
public static void beforeClass() throws Exception {
System.out.println("测试类执行之前执行,主要用来初使化公共资源等");
}
@AfterClass
public static void afterClass() throws Exception {
System.out.println("测试类执行之后执行,主要用来释放资源或清理工作");
}
@Before
public void setup() throws Exception {
System.out.println("测试方法执行之前执行");
}
@After
public void teardown() throws Exception {
System.out.println("测试方法执行之后执行");
}
@Test
public void test() {
System.out.println("测试方法");
}
@Test
@Ignore("可以忽略这个方法")
public void testIgnore() {
System.out.println("测试忽略方法");
}
@Test(expected = ArithmeticException.class)
public void divisionWithException() {
System.out.println("测试异常");
int i = 1 / 0;
}
@Test(timeout = 1000)
public void infinity() {
System.out.println("测试超时");
while (true) ;
}
}
2、常用断言
断言 | 说明 |
---|---|
assertArrayEquals(expecteds, actuals) | 查看两个数组是否相等。 |
assertEquals(expected, actual) | 查看两个对象是否相等。类似于字符串比较使用的equals()方法 |
assertNotEquals(first, second) | 查看两个对象是否不相等。 |
assertNull(object) | 查看对象是否为空。 |
assertNotNull(object) | 查看对象是否不为空。 |
assertSame(expected, actual) | 查看两个对象的引用是否相等。类似于使用“==”比较两个对象 |
assertNotSame(unexpected, actual) | 查看两个对象的引用是否不相等。类似于使用“!=”比较两个对象 |
assertTrue(condition) | 查看运行结果是否为true。 |
assertFalse(condition) | 查看运行结果是否为false。 |
assertThat(actual, matcher) | 查看实际值是否满足指定的条件 |
fail() | 让测试失败 |
Mockit模拟测试框架
Mockito 是一个强大的用于 Java 开发的模拟测试框架, 通过 Mockito 我们可以创建和配置 Mock 对象, 进而简化有外部依赖的类的测试.
1、Mockito框架的好处
-
可以很简单的虚拟出一个复杂对象(比如虚拟出一个接口的实现类);
-
可以配置 mock 对象的行为;
-
可以使测试用例只注重测试流程与结果;
-
减少外部类、系统和依赖给单元测试带来的耦合。
2、使用 Mockito 的大致流程如下
-
创建外部依赖的 Mock 对象, 然后将此 Mock 对象注入到测试类中.
-
执行测试代码.
-
校验测试代码是否执行正确
3、什么是mock?
mock就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法,这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品。
如图所示,ConsumableQueryService耗材查询服务依赖ConsumableTransfer和ConsumableRemoteService,现在需要测试耗材查询服务,一种方法是构建真实的 ConsumableTransfer,和ConsumableRemoteService 实例, 然后注入到 ConsumableQueryService 中,这样就违背了单元测试只测试程序单元自身的功能的原则,这时候我们就需要使用mock对象来进行替代了。替换后, 我们就可以对 ConsumableQueryService 进行测试, 并且不需要关注它的复杂的依赖。
4、Mockito使用
使用MockitoAnnotations模拟对象
public class ConsumableQueryServiceTest {
@InjectMocks
private ConsumableQueryService consumableQueryService;
@Mock
private ConsumableTransfer consumableTransfer;
@Mock
private ConsumableRemoteService consumableRemoteService;
@Before
public void initMock() {
MockitoAnnotations.initMocks(this);
ConsumableBatchesQO qo = new ConsumableBatchesQO();
qo.setChannelId(1);
qo.setWarehouseId(1);
qo.setKeyword("耗材");
qo.setCascaded(true);
qo.setNeedOutOfDate(Boolean.FALSE);
List consumableBatchesInfos = new ArrayList<>();
ConsumableBatchesInfo batchesInfo = new ConsumableBatchesInfo();
batchesInfo.setUnitId(-1);
batchesInfo.setName("耗材测试");
ConsumableClinicInfo channelInfo = new ConsumableClinicInfo();
batchesInfo.setChannelInfo(channelInfo);
consumableBatchesInfos.add(batchesInfo);
when(consumableRemoteService.findBatches(qo)).thenReturn(consumableBatchesInfos);
ConsumableBO consumableBO = new ConsumableBO();
consumableBO.setId(batchesInfo.getUnitId());
consumableBO.setName(batchesInfo.getName());
when(consumableTransfer.toConsumableBO(consumableBatchesInfos.get(0))).thenReturn(consumableBO);
}
@Test
public void testQueryByKeyWord() {
List consumables = consumableQueryService.queryByKeyWord(1, 1, "耗材");
Assert.assertEquals(consumables.get(0).getName(), "耗材测试");
}
}
使用MockitoJUnitRunner模拟对象
@RunWith(MockitoJUnitRunner.class)
public class ConsumableQueryServiceTest {
@InjectMocks
private ConsumableQueryService consumableQueryService;
@Mock
private ConsumableTransfer consumableTransfer;
@Mock
private ConsumableRemoteService consumableRemoteService;
@Before
public void initMock() {
ConsumableBatchesQO qo = new ConsumableBatchesQO();
qo.setChannelId(1);
qo.setWarehouseId(1);
qo.setKeyword("耗材");
qo.setCascaded(true);
qo.setNeedOutOfDate(Boolean.FALSE);
List consumableBatchesInfos = new ArrayList<>();
ConsumableBatchesInfo batchesInfo = new ConsumableBatchesInfo();
batchesInfo.setUnitId(-1);
batchesInfo.setName("耗材测试");
ConsumableClinicInfo channelInfo = new ConsumableClinicInfo();
batchesInfo.setChannelInfo(channelInfo);
consumableBatchesInfos.add(batchesInfo);
when(consumableRemoteService.findBatches(qo)).thenReturn(consumableBatchesInfos);
ConsumableBO consumableBO = new ConsumableBO();
consumableBO.setId(batchesInfo.getUnitId());
consumableBO.setName(batchesInfo.getName());
when(consumableTransfer.toConsumableBO(consumableBatchesInfos.get(0))).thenReturn(consumableBO);
}
@Test
public void testQueryByKeyWord() {
List consumables = consumableQueryService.queryByKeyWord(1, 1, "耗材");
Assert.assertEquals(consumables.get(0).getName(), "耗材测试");
}
}
使用MockitoRule模拟对象
public class ConsumableQueryServiceTest {
@InjectMocks
private ConsumableQueryService consumableQueryService;
@Mock
private ConsumableTransfer consumableTransfer;
@Mock
private ConsumableRemoteService consumableRemoteService;
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Before
public void initMock() {
ConsumableBatchesQO qo = new ConsumableBatchesQO();
qo.setChannelId(1);
qo.setWarehouseId(1);
qo.setKeyword("耗材");
qo.setCascaded(true);
qo.setNeedOutOfDate(Boolean.FALSE);
List consumableBatchesInfos = new ArrayList<>();
ConsumableBatchesInfo batchesInfo = new ConsumableBatchesInfo();
batchesInfo.setUnitId(-1);
batchesInfo.setName("耗材测试");
ConsumableClinicInfo channelInfo = new ConsumableClinicInfo();
batchesInfo.setChannelInfo(channelInfo);
consumableBatchesInfos.add(batchesInfo);
when(consumableRemoteService.findBatches(qo)).thenReturn(consumableBatchesInfos);
ConsumableBO consumableBO = new ConsumableBO();
consumableBO.setId(batchesInfo.getUnitId());
consumableBO.setName(batchesInfo.getName());
when(consumableTransfer.toConsumableBO(consumableBatchesInfos.get(0))).thenReturn(consumableBO);
}
@Test
public void testQueryByKeyWord() {
List consumables = consumableQueryService.queryByKeyWord(1, 1, "耗材");
Assert.assertEquals(consumables.get(0).getName(), "耗材测试");
}
}
5、Mockit常用注解
-
@InjectMocks进行依赖注入
-
@Mock注解,我们用来初始化Mock对象
-
@Captor参数捕获器的注解
-
@Spy包装Java对象
6、Mockit常用方法
verify函数
verify函数默认验证的是执行了times(1),也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了。
Method | Meaning |
---|---|
times(n) | 次数为n,默认为1(times(1)) |
never() | 次数为0,相当于times(0) |
atLeast(n) | 最少n次 |
atLeastOnce() | 最少一次 |
atMost(n) | 最多n次 |
实列化虚拟对象
@Before
public void initMock() {
ConsumableBatchesQO qo = new ConsumableBatchesQO();
qo.setChannelId(1);
qo.setWarehouseId(1);
qo.setKeyword("耗材");
qo.setCascaded(true);
qo.setNeedOutOfDate(Boolean.FALSE);
List consumableBatchesInfos = new ArrayList<>();
ConsumableBatchesInfo batchesInfo = new ConsumableBatchesInfo();
batchesInfo.setUnitId(-1);
batchesInfo.setName("耗材测试");
ConsumableClinicInfo channelInfo = new ConsumableClinicInfo();
batchesInfo.setChannelInfo(channelInfo);
consumableBatchesInfos.add(batchesInfo);
//实例化虚拟对象
when(consumableRemoteService.findBatches(qo)).thenReturn(consumableBatchesInfos);
consumableRemoteService.findBatches(qo);
consumableRemoteService.findBatches(qo);
//校验次数
verify(consumableRemoteService, times(2)).findBatches(qo);
ConsumableBO consumableBO = new ConsumableBO();
consumableBO.setId(batchesInfo.getUnitId());
consumableBO.setName(batchesInfo.getName());
when(consumableTransfer.toConsumableBO(consumableBatchesInfos.get(0))).thenReturn(consumableBO);
}