技术经验分享:谈一谈单元测试(2)

发表于:2022-2-18 09:07

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:有尘    来源:阿里技术

  三、 Junit5和Mockito
  后面讲到的自动生成使用的框架和业界使用最多的都是MocKito,所以这里重点介绍一下,包括使用时遇到的问题。
  1. 使用方法
  分别单独引入依赖,推荐引入最新版
  <!-- junit5 -->
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.7.2</version>
    <scope>test</scope>
  </dependency>
  <!-- mockito -->
  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.9.0</version>
    <scope>test</scope>
  </dependency>
  <!-- mockito 的junit5适配器 -->
  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>3.9.0</version>
    <scope>test</scope>
  </dependency>

  使用spring-test全家桶
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <version>2.5.0</version>
  </dependency>

  junit5的使用方法这里就不多做介绍,主要说一下这个ArgumentsProvider接口,实现它就可以自定义参数化类,类似于自带的ValueSource、EnumSource等。
  2. Mockito 主要注解介绍
  先问为什么,为什么需要Mockito。
  因为:现在的java项目几乎离不开spring框架,而其最为著名的就是IOC,所有的bean用容器来管理,所以这给我们单元测试带来一个问题,如果要对bean做单元测试,就需要启动容器,那么带来的时间的开销将会很大。所以Mockito给我门带来了一系列的解决方法,让我们可以轻松的对bean 进行测试。
  @Component
  public class A {
      @Autowired
      private B b; // 完全mock
      @Autowired
      private C c; // 需要执行方法
      @Autowired D d; // 需要执行真实方法
      public void func(){
      }
  }
  @Component
  class C {
      @Autowired
      private B b;
      public void needExec(){
      }
  }
  @Component
  public class B {
  }

  假设我们要对上面的A.func()进行单元测试。
  @InjectMocks注解
  表示需要注入bean的类,有两种
  ·被测试类,这种很容易理解,我们测试这个类,当然也需要向其注入bean。比如上面的A
  · 被测试类中的,需要执行其真实的方法,但其里面也要主要bean,也就是上面的C,我们需要测试neeExec方法,但我们不关系B的具体细节。现实中比如事物,并发锁等。这一类需要Mockito.spy(new C())的形式,不然会报错
  @Mock
  表示要mock的数据,也就是不真实执行其方法内容,只按照我们的规则执行,或者返回,比如使用when().thenReturn()语法。
  当然也可以,执行真实方法,则需要when().thenCallRealMethod()方式。
  @Spy
  表示所有方法都走真实方式,比如有些工具类,转换类,我们也写成了bean的形式(严格来说这种需要写成静态工具类)。
  @ExtendWith(MockitoExtension.class)
  public class ATest  {
    @InjectMocks
    private A a=new A(); 
    @Mock
    private B b;
    @Spy
    private D d;
    @InjectMocks
    private C c= Mockito.spy(new C());;
    @BeforeEach
    public void setUp() throws Exception {
      MockitoAnnotations.openMocks(this);
    }
    @ParameterizedTest
    @ValueSource(strings = {"/com/alibaba/cq/springtest/jcode5test/needMockService/A/func.json"})
     public void funcTest(String str) {
      JSONObject arg= TestUtils.getTestArg(str);
      a.func();
      //todo verify the result
    }
  }

  3. Mockito和junit5常见问题
  mock静态方法
  mockito3.4以后开始支持,之前的版本可以使用PowerMock辅助使用
  Mockito版本和java版本兼容问题
  报错如下:
  Mockito cannot mock this class: xxx
  Mockito can only mock non-private & non-final classes.

  原因是2.17.0及之前的版本与java8是兼容的。
  但2.18之后需要使用java11,为了在java8中使用Mockito,则需要引入另一个包。
  <dependency>
      <groupId>net.bytebuddy</groupId>
      <artifactId>byte-buddy</artifactId>
      <version>1.12.6</version>
  </dependency>

  Jupiter-api版本兼容问题
  Process finished with exit code 255
  java.lang.NoSuchMethodError: org.junit.jupiter.api.extension.ExtensionContext.getRequiredTestInstances()Lorg/junit/jupiter/api/extension/TestInstance

  第一个问题是因为junit5中api、engine、params版本不一致导致的。
  第二个问题是因为jupiter-api版本太低的问题,5.7.0以后的版本才支持。
  四 、测试代码自动生成
  选好了框架,我们还是没有解决我们的问题,“怎么节约开发成本?” ,这一节我们来谈这个问题,这也是我主要想表达的。
  对于写单元测试,一直以来是比较头痛的事情,要组装各种各样的数据,可能还没跑成功,就被一堆“xxxx不能为null”的报错搞烦了。因此我们有理由去设想,有没有办法去解决这件事情。
  1. 业界和集团方案调研
  在做这个事情之前,肯定是要调研有没有现成的框架。答案是有,但很遗憾,没有找到完全契合我想要的效果,我们来看一下这些插件:
  public class BaseTest {
      protected TestService testService;
      public String baseTest() {
          return testService.testBase(1); // 4
      }
  }
  public class JCode5 extends BaseTest {
       public void testExtend(){
          String s = testService.testOther(new Student()); //1 
          // 调用 另一个方法
          System.out.println(testBean());
          // 调用基类方法
          baseTest();
      }
      // 使用testService
      public String testBean() {
          testService.testMuti(new ArrayList<Integer>() {{add(1);}}, 2); //2
          return testService.getStr(12); //3
      }
      /**
       * 测试范型类
       */
      public void testGeneric(Person person) {
          //test
          list.stream().forEach(a -> {
              System.out.println(a);
          });
          for (int i = 0; i < 2; i++) {
              Long aLong = testService.getLong("1213"
                  , "12323");
              System.out.println(aLong);
          }
          System.out.println(testBean());
      }
  }
  public class TestService {
      public String testBase(Integer integer) {
          return "TestBase";
      }
      public List<String> testMuti(List<Integer> a, Integer c) {
          List<String> res = new ArrayList<>();
          res.add(a.toString() + c + "test muti");
          return res;
      }
       public String getStr(Integer integer) {
          return "TestService" + getInt();
      }
       public String testOther(Student student) {
          return student.getAge() + "age";
      }
  }

  如上,testExtend一共调用了testService的4个方法,我们对比下各个插件生成的代码。
  TestMe
  @Test
      void testTestExtend() {
          when(testService.getStr(anyInt())).thenReturn("getStrResponse");
          when(testService.testMuti(any(), anyInt())).thenReturn(Arrays.<String>asList("String"));
          when(testService.testOther(any())).thenReturn("testOtherResponse");
          jCode5.testExtend(Integer.valueOf(0));
      }
      @Test
      void testTestGeneric() {
          when(testService.getStr(anyInt())).thenReturn("getStrResponse");
          when(testService.getLong(anyString(), anyString())).thenReturn(Long.valueOf(1));
          when(testService.testMuti(any(), anyInt())).thenReturn(Arrays.<String>asList("String"));
          jCode5.testGeneric(new Person());
      }

  生成的代码基本符合逻辑,包括需要mock的bean的逻辑都生成了。
  ·但它把最重要的一环,也就是数据省略了,只是单纯的用了构造函数的形式。这显然对于我们DDD模型不适应。
  · 另外他没用用到junit5的一些特性,比如参数化测试。
  · 对于testExtend的方法,它只识别了3个方法。没有识别父类的调用。
  JunitGenerate
  只能生成基础的框架代码,对于我想mock的逻辑、以及测试方法都没有生成,用处不大。
  @Test
  public void testTestExtend() throws Exception { 
  //TODO: Test goes here... 
  }

  Squaretest
  生成的方法非常丰富,且一个非常厉害的一点,它能生成多个分支,比如代码逻辑中有if条件,它能生成两个测试,从而走不通的分支。
  但是,最大的缺点是“收费软件,不开源”,这就决定了我们没法用它,除非是特别需要。另外测试用过程中还发现了一些其他问题,比如对于继承,重载之类的问题,它解决的也不是很好,往往识别不了需要调用的方法。
  虽然无法使用,但还是可以借鉴。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号