根据集成测试用例补充单元测试用例
在之前的测试旅程中,我们新建了测试计划并将测试用例纳入该计划来执行。以下是上述用例执行之后对添加测试计划的一个代码覆盖率。
可以看到,由于只是调用了TestPlanService的addTestPlan方法,整体这个Service类的覆盖率还是比较低的。即使在addTestPlan这个方法的内部,也是存在着不少未被测试到的业务逻辑。因此,通过单元测试来补充测试覆盖也是一种质量内建的有效方式。
补充用例1-测试计划名称重复异常
来看一下addTestPlan中中第一个if的代码。从设计上来讲,这是一个哨兵断言,当存在重复的测试计划名称时,可以直接抛异常退出,提高程序处理效率。由于集成测试中的场景是测试计划被成功创建,因此这个if判断并没有进入,而是进入了继续创建测试计划的逻辑。
因此,我们需要在此处补充一个因为测试计划名称重复导致测试计划创建失败的案例。一般来说,如果是系统测试或者集成测试,我们可以通过尝试创建两个相同名字的测试计划来验证这一逻辑。不过就单元测试来说,则可以通过模拟的方式来实现。
首先来看一下系统界定存在重复的测试计划名称的方式。在getTestPlanByName方法中,通过查询数据库的方式,验证在给定的workspace中是否存在给定的测试计划名称,如果存在则返回查询到的测试计划列表。
因此,判定是否重名的逻辑就是,数据库查询返回的列表包含的记录数是否大于0。如果大于则表明存在重名,程序抛出异常。
测试用例-第一版
因此,我们设计一个测试用例,来模拟测试计划重名的场景。
Given- 新建测试计划
When- 根据给定测试计划名称查询数据库返回不为空
Then-抛出异常
根据这个场景,我们来编写一下测试用例
代码语言:javascript复制 package io.metersphere.track.service;
import io.metersphere.base.domain.TestPlan;
import io.metersphere.base.domain.TestPlanExample;
import io.metersphere.base.mapper.TestPlanMapper;
import io.metersphere.track.request.testplan.AddTestPlanRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(SpringExtension.class)
@ExtendWith(MockitoExtension.class)
public class TestPlanServiceTest {
@InjectMocks
TestPlanService testPlanService;
@Mock
TestPlanMapper testPlanMapper;
@Test
public void testPlanNameShouldNotDuplicate(){
AddTestPlanRequest addTestPlanRequest= new AddTestPlanRequest();
TestPlan testPlan=new TestPlan();
List<TestPlan> testPlans= Arrays.asList(testPlan);
Mockito.when(testPlanMapper.selectByExample(Mockito.any(TestPlanExample.class)))
.thenReturn(testPlans);
assertThatThrownBy(()-> testPlanService.addTestPlan(addTestPlanRequest)).hasMessage("plan_name_already_exists");
}
}
执行一下,发现居然空指针异常了
原来在准备数据库查询语句的代码中有如下的一行,
代码语言:javascript复制 example.createCriteria().andWorkspaceIdEqualTo(SessionUtils.getCurrentWorkspaceId())
.andNameEqualTo(name);
由于我们是单元测试,并没有启动Spring容器,也没有Session,因此SessionUtils.getCurrentWorkspaceId()运行的结果是Null,而andWorkspaceIdEqualTo(String workSpaceId)方法中如果入参为null,则会抛出空指针异常。
代码语言:javascript复制 protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " property " cannot be null");
}
criteria.add(new Criterion(condition, value));
}
类似的问题还有
代码语言:javascript复制 Translator.get("plan_name_already_exists")
这个方法的定义是这样的
代码语言:javascript复制 package io.metersphere.i18n;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import javax.annotation.Resource;
public class Translator {
private static MessageSource messageSource;
@Resource
public void setMessageSource(MessageSource messageSource) {
Translator.messageSource = messageSource;
}
/**
* 单Key翻译
*/
public static String get(String key) {
return messageSource.getMessage(key, null, "Not Support Key", LocaleContextHolder.getLocale());
}
}
它是一个静态方法,用于对给定的信息,根据Locale来提供一个本地化翻译。由于执行翻译的是MessageSource,而set方法是委托给了Spring容器在初始化时完成,并不允许在runtime时动态指定。
因此,一个看似只有2-3行的代码段,在使用Mockito造完测试桩之后,我们发现还有2个静态方法需要处理才能实现最初的测试目的,模拟测试计划名称重名的场景。
测试用例-Mockito-Inline登场
在使用Mockito来mock testPlanMapper模拟数据库返回的基础上,还需要额外对以下两个两个静态方法的调用进行Mock。
代码语言:javascript复制 SessionUtils.getCurrentWorkspaceId()
Translator.get(expected)
当然,这里使用的是Mockito3最新提供的Mockito-Inline,这个包提供了mock静态方法的能力,只是目前还没有被吸收进Mockito-core中,因此,需要将Mockito的依赖修改为对Mockito-Inline的依赖。
修改之后的用例如下,
代码语言:javascript复制 package io.metersphere.track.service;
import io.metersphere.base.domain.TestPlan;
import io.metersphere.base.domain.TestPlanExample;
import io.metersphere.base.mapper.TestPlanMapper;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.i18n.Translator;
import io.metersphere.track.request.testplan.AddTestPlanRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(SpringExtension.class)
@ExtendWith(MockitoExtension.class)
public class TestPlanServiceTest {
@InjectMocks
TestPlanService testPlanService;
@Mock
TestPlanMapper testPlanMapper;
@Test
public void testPlanNameShouldNotDuplicate(){
String expected ="plan_name_already_exists";
AddTestPlanRequest addTestPlanRequest= new AddTestPlanRequest();
addTestPlanRequest.setName("NeedPlanNameHere");
TestPlan testPlan=new TestPlan();
List<TestPlan> testPlans= Arrays.asList(testPlan);
Mockito.when(testPlanMapper.selectByExample(Mockito.any(TestPlanExample.class)))
.thenReturn(testPlans);
try (MockedStatic<Translator> translator= Mockito.mockStatic(Translator.class);
MockedStatic<SessionUtils> sessionUtils= Mockito.mockStatic(SessionUtils.class);
){
sessionUtils.when(() -> { SessionUtils.getCurrentWorkspaceId();}).thenReturn("NeedWorkSpaceIdHere");
translator.when(() -> Translator.get(expected)).thenReturn(expected);
assertThatThrownBy(()-> testPlanService.addTestPlan(addTestPlanRequest))
.isInstanceOf(MSException.class)
.hasMessage(expected);
}
}
}
上述案例中,当进行单元测试时,由于缺少Session以及某些Spring托管的服务,造成了用例执行失败。因此,额外引入了Mockito-Inline来mock 静态方法让整个测试桩能符合测试场景的要求,并最终执行成功。