专栏首页软件测试那些事MeterSphere单元测试-Mockito-Inline出场

MeterSphere单元测试-Mockito-Inline出场

根据集成测试用例补充单元测试用例

在之前的测试旅程中,我们新建了测试计划并将测试用例纳入该计划来执行。以下是上述用例执行之后对添加测试计划的一个代码覆盖率。

可以看到,由于只是调用了TestPlanService的addTestPlan方法,整体这个Service类的覆盖率还是比较低的。即使在addTestPlan这个方法的内部,也是存在着不少未被测试到的业务逻辑。因此,通过单元测试来补充测试覆盖也是一种质量内建的有效方式。

补充用例1-测试计划名称重复异常

来看一下addTestPlan中中第一个if的代码。从设计上来讲,这是一个哨兵断言,当存在重复的测试计划名称时,可以直接抛异常退出,提高程序处理效率。由于集成测试中的场景是测试计划被成功创建,因此这个if判断并没有进入,而是进入了继续创建测试计划的逻辑。

因此,我们需要在此处补充一个因为测试计划名称重复导致测试计划创建失败的案例。一般来说,如果是系统测试或者集成测试,我们可以通过尝试创建两个相同名字的测试计划来验证这一逻辑。不过就单元测试来说,则可以通过模拟的方式来实现。

首先来看一下系统界定存在重复的测试计划名称的方式。在getTestPlanByName方法中,通过查询数据库的方式,验证在给定的workspace中是否存在给定的测试计划名称,如果存在则返回查询到的测试计划列表。

因此,判定是否重名的逻辑就是,数据库查询返回的列表包含的记录数是否大于0。如果大于则表明存在重名,程序抛出异常。

测试用例-第一版

因此,我们设计一个测试用例,来模拟测试计划重名的场景。

Given- 新建测试计划

When- 根据给定测试计划名称查询数据库返回不为空

Then-抛出异常

根据这个场景,我们来编写一下测试用例

 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");
     }
 }

执行一下,发现居然空指针异常了

原来在准备数据库查询语句的代码中有如下的一行,

         example.createCriteria().andWorkspaceIdEqualTo(SessionUtils.getCurrentWorkspaceId())
                 .andNameEqualTo(name);

由于我们是单元测试,并没有启动Spring容器,也没有Session,因此SessionUtils.getCurrentWorkspaceId()运行的结果是Null,而andWorkspaceIdEqualTo(String workSpaceId)方法中如果入参为null,则会抛出空指针异常。

 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));
 }

类似的问题还有

 Translator.get("plan_name_already_exists")

这个方法的定义是这样的

 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。

 SessionUtils.getCurrentWorkspaceId()
 Translator.get(expected)

当然,这里使用的是Mockito3最新提供的Mockito-Inline,这个包提供了mock静态方法的能力,只是目前还没有被吸收进Mockito-core中,因此,需要将Mockito的依赖修改为对Mockito-Inline的依赖。

修改之后的用例如下,

 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 静态方法让整个测试桩能符合测试场景的要求,并最终执行成功。

本文分享自微信公众号 - 软件测试那些事(antony-not-available),作者:风月同天测试人

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-12-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 2021第一篇-流量录制回放完整案例

    在之前的《录制回放实现测试用例自由》一文中,笔者简单介绍了如何通过切面来录制HTTP接口请求和返回,并实现了用例的回放。 当然,在实际的项目中,对于应用来说,除...

    Antony
  • 如何用Junit5玩出参数化测试的新花样?

    这是之前一篇文章《用junit5编写一个类ZeroCode的测试框架》的续集。主要将在之前工作的基础上,围绕参数化测试展开。 框架主要设计点:

    Antony
  • 使用MockMVC进行Controller单元测试

    由于MockMVC是Spring框架自带的测试组件,因此只要在项目中引入spring-boot-starter-test这个测试套件就可以使用Spring-te...

    Antony
  • Spring Cloud微服务技术栈(五):客户端负载均衡Spring Cloud Ribbon部分源码分析

    为了使客户端具备负载均衡的能力,我们在代码中将RestTemplate交给Spring管理的时候,会加上@LoadBalanced注解,如下代码所示:

    itlemon
  • Ribbon与Spring cloud整合源码分析

    Ribbon是一种客户端的负载均衡器。提供了多种负载均衡的算法,支持多种协议(HTTP,TCP,UDP),并提供了故障容错的能力。官方网址为:https://g...

    良辰美景TT
  • spring中通过配置文件注入的方法

    2.通过配置文件注入的方法 上面的注入方法是通过@Service的注解方法。类似的还有@Repository、@Component、@Constroller,功...

    马克java社区
  • 资源读取配置 原

    南郭先生
  • SpringBoot之使用jpa/hibernate

        bootstrap.yml内容如下,我们不需要手动创建数据库表,jpa/hiberate会自动会为我们创建的

    克虏伯
  • Eureka 服务提供者与消费者

    1:修改pom.xml文件,与服务提供都不同的是这里需要引入spring-cloud-starter-ribbon 用于做负载均衡。服务提供都可能会部署多个实例...

    良辰美景TT
  • SpringBoot使用WebFlux响应式编程操作数据库

    在之前一篇简单介绍了WebFlux响应式编程的操作,我们在来看一下下图,可以看到,在目前的Spring WebFlux还没有支持类似Mysql这样的关系型数据库...

    dalaoyang

扫码关注云+社区

领取腾讯云代金券