网络知识 娱乐 单元测试工具预研

单元测试工具预研

商业工具:

  • AgitarOne似乎是该领域最大的参与者。 还有一个30天的试用版和名为JUnit Factory的免费Web界面。
    例如,此处对工具的用法进行了独立审查。
    AgitarOne的测试生成主要集中在回归测试上。(申请免费试用需要企业邮箱,还需要走申请流程,没有申请成功)

免费工具:

  • EvoSuite赢得了SBST(基于搜索的软件测试)13届单元测试生成竞赛以及SBST 17届单元测试生成竞赛的冠军。
    存在该工具的Eclipse插件,Web界面和命令行版本。 有记录的回归标准,测试生成了分支覆盖率,弱突变覆盖率或强突变覆盖率的目标。

  • Randoop使用反馈导向的随机测试生成方法。 该工具自2007年问世以来一直在不断发展。
    Randoop可以同时进行回归测试和错误查找测试。 这是一个强大且可靠的命令行工具,并且还存在Eclipse插件。

  • Squaretest,是一款IDEA插件,收费,基于Velocity模板生成单测,会帮每个对应的被测方法生成一个测试类,spring的注入类会被mock掉,没有参数推导,所以参数还需要用户自行填充

  • diffblue 是一款基于AI来编写单测的工具,它分析您现有的Java应用程序,并编写反映当前行为的单元测试,从而增加测试范围并帮助您在将来的代码更改中查找回归。Cover在代码更改时通过更新测试来自动维护测试。Cover支持标准Java 8和11,Spring和Spring Boot,但是需要springboot 2.x起,重要的是它是免费的,支持CLI和IDEA插件,当然它也有收费版,提供更加强大的生成功能,但是开发版每人每月高达75美元

以下工具测试环境:
被测代码:https://gitee.com/Dray/unit-test.git
IDEA:2021.2.3
jdk:1.8
springboot:2.6.0 j
unit:4.1.2

Squaretest介绍

squaretest是一款基于IDEA插件生成测试用例的工具但是收费的,不过个人版35美元可以获得永久激活码,安装方式非常简单直接idea插件市场搜索安装即可
在这里插入图片描述
在这里插入图片描述

它支持多种模板的用例生成,当然我们也可以按照它的规范去自定义我们的模板

package com.netease.evo.squaretest;

import com.netease.evo.dto.CourseDto;
import com.netease.evo.dto.UserDto;
import com.netease.evo.dto.UserQueryDto;
import com.netease.evo.service.CourseService;
import com.netease.evo.service.impl.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {

    @Mock
    private CourseService mockCourseService;

    @InjectMocks
    private UserServiceImpl userServiceImplUnderTest;

    @Test
    void testGetUserById() {
        // Setup
        final UserDto expectedResult = new UserDto(0, "username", "email", 0, 0);

        // Run the test
        final UserDto result = userServiceImplUnderTest.getUserById(0);

        // Verify the results
        assertThat(result).isEqualTo(expectedResult);
    }

    @Test
    void testQueryUser() {
        // Setup
        final UserQueryDto query = new UserQueryDto("username", 0);
        final List expectedResult = Arrays.asList(new UserDto(0, "username", "email", 0, 0));

        // Run the test
        final List result = userServiceImplUnderTest.queryUser(query);

        // Verify the results
        assertThat(result).isEqualTo(expectedResult);
    }

    @Test
    void testGetCourseById() {
        // Setup
        final List expectedResult = Arrays.asList(new CourseDto(0, "name"));
        when(mockCourseService.getByUserId(0)).thenReturn(Arrays.asList(new CourseDto(0, "name")));

        // Run the test
        final List result = userServiceImplUnderTest.getCourseById(0);

        // Verify the results
        assertThat(result).isEqualTo(expectedResult);
    }

    @Test
    void testGetCourseById_CourseServiceReturnsNoItems() {
        // Setup
        final List expectedResult = Arrays.asList(new CourseDto(0, "name"));
        when(mockCourseService.getByUserId(0)).thenReturn(Collections.emptyList());

        // Run the test
        final List result = userServiceImplUnderTest.getCourseById(0);

        // Verify the results
        assertThat(result).isEqualTo(expectedResult);
    }
}

通过查看生成的用例,我们可以看出squaretest能帮我们自动mock掉依赖包,用例参数没有基于代码分析得到,只是简单填充了类型的默认值,所以这个工具能帮我们省下创建每个方法用例方法的时间,需要我们手动去填充用例参数值,只是一定程度上减少单测编写。

EvoSuite介绍

EvoSuite是由 Sheffield(谢菲尔德) 等大学联合开发的一种开源工具,用于自动生成测试用例集,生成的测试用例均符合 Junit 的标准,可直接在 Junit 中运行。
通过使用此自动测试工具能够在保证代码覆盖率的前提下极大地提高测试人员的开发效率。但是只能辅助测试,并不能完全取代人工,测试用例的正确与否还需人工判断。
EvoSuite自2018年4月停止维护后,2020年10月恢复维护,主要添加了优化代码,修复bug,以及增加了 Java 9+ 的支持,最新版本为1.2版本增加了Junit5的支持,但实际使用时发现默认还是junit4
核心功能

  • Generation of JUnit 4 tests for the selected classes 生成指定类的 Junit 4 测试用例
  • Optimization of different coverage criteria, like lines, branches, outputs and mutation testing 通过不同的覆盖指标调整生成的用例,如行覆盖率、- 分支覆盖率、输出及变异测试(mutation testing)
  • Tests are minimized: only the ones contributing to achieve coverage are retained 测试最小化,只有能贡献覆盖指标的用例才会被保留下来
  • Generation of JUnit asserts to capture the current behavior of the tested classes 生成 Junit 断言来检验被测试的类的行为
  • Tests run in a sandbox to prevent potentially dangerous operations 测试被运行在一个沙盒中,避免潜在的危险行为
  • Virtual file system 虚拟文件系统
  • Virtual network 虚拟网络
    官方提供了包括命令行工具、eclipse 插件、idea 插件、maven 插件 在内的数种运行方式
    其中Idea 插件早已停止维护,maven插件最新版中央仓库也无法找到,需要自己构建,整体来说,文档和包基本没有维护。
    下载源码
    首先从github下载EvoSuite的源码,然后构建,将包插入本地仓库
    引入maven插件

    org.evosuite
    evosuite-standalone-runtime
    ${evosuite.version}
    test

#如果使用的是高版本的springboot-test,需要单独引入junit4


    junit
    junit
    4.12
    test



    org.evosuite.plugins
    evosuite-maven-plugin
    ${evosuite.version}
    
        
            
                prepare
            
            process-test-classes
        
    

运行命令

mvn compile -DmemoryInMB=2000 -Dcores=2 -Dcuts=com.netease.evo.service.impl.UserServiceImpl evosuite:generate evosuite:export

compile 表示编译。evosuite 是基于编译后的 .class 文件生成用例的,所以需要先编译。
-DmemoryInMB=2000 表示使用 2000MB 的内存
-Dcores=2 表示用 2 个 cpu 来并行加快生成速度
-Dcuts=alexp.blog.service.PostServiceImpl 表示只针对 alexp.blog.service.PostServiceImpl 这个类生成用例。多个用例可以用英文逗号分隔,不幸的是它并不支持文件夹的形式读取源码
-DtargetFolder=src/test/java/evosuite 表示生成的用例放到 src/test/java/evosuite 。
evosuite:generate 表示执行生成用例
evosuite:export 表示导出用例到 targetFolder 的值所在的目录中
可以看到自动帮我们生成了单元测试
在这里插入图片描述

/*
 * This file was automatically generated by EvoSuite
 * Thu Nov 25 03:28:38 GMT 2021
 */

package com.netease.evo.service.impl;

import com.netease.evo.dto.UserDto;
import com.netease.evo.dto.UserQueryDto;
import org.evosuite.runtime.EvoRunner;
import org.evosuite.runtime.EvoRunnerParameters;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.List;

import static org.evosuite.runtime.EvoAssertions.verifyException;
import static org.junit.Assert.*;

@RunWith(EvoRunner.class) @EvoRunnerParameters(mockJVMNonDeterminism = true, useVFS = true, useVNET = true, resetStaticState = true, separateClassLoader = true) 
public class UserServiceImpl_ESTest extends UserServiceImpl_ESTest_scaffolding {

  @Test(timeout = 4000)
  public void test0()  throws Throwable  {
      UserServiceImpl userServiceImpl0 = new UserServiceImpl();
      UserQueryDto userQueryDto0 = new UserQueryDto();
      // Undeclared exception!
      try { 
        userServiceImpl0.queryUser(userQueryDto0);
        fail("Expecting exception: NullPointerException");
      
      } catch(NullPointerException e) {
         //
         // no message in exception (getMessage() returned null)
         //
         verifyException("com.netease.evo.service.impl.UserServiceImpl", e);
      }
  }

  @Test(timeout = 4000)
  public void test1()  throws Throwable  {
      UserServiceImpl userServiceImpl0 = new UserServiceImpl();
      // Undeclared exception!
      try { 
        userServiceImpl0.getUserById((Integer) null);
        fail("Expecting exception: NullPointerException");
      
      } catch(NullPointerException e) {
         //
         // no message in exception (getMessage() returned null)
         //
         verifyException("com.netease.evo.service.impl.UserServiceImpl", e);
      }
  }

  @Test(timeout = 4000)
  public void test2()  throws Throwable  {
      UserServiceImpl userServiceImpl0 = new UserServiceImpl();
      Integer integer0 = new Integer(2);
      UserQueryDto userQueryDto0 = new UserQueryDto("", integer0);
      List list0 = userServiceImpl0.queryUser(userQueryDto0);
      assertFalse(list0.isEmpty());
  }

  @Test(timeout = 4000)
  public void test3()  throws Throwable  {
      UserServiceImpl userServiceImpl0 = new UserServiceImpl();
      Integer integer0 = new Integer(1);
      UserQueryDto userQueryDto0 = new UserQueryDto("", integer0);
      List list0 = userServiceImpl0.queryUser(userQueryDto0);
      assertEquals(1, list0.size());
  }

  @Test(timeout = 4000)
  public void test4()  throws Throwable  {
      UserServiceImpl userServiceImpl0 = new UserServiceImpl();
      Integer integer0 = new Integer(19);
      UserDto userDto0 = userServiceImpl0.getUserById(integer0);
      assertNull(userDto0);
  }

  @Test(timeout = 4000)
  public void test5()  throws Throwable  {
      UserServiceImpl userServiceImpl0 = new UserServiceImpl();
      Integer integer0 = new Integer(2);
      UserDto userDto0 = userServiceImpl0.getUserById(integer0);
      assertEquals(19, (int)userDto0.getAge());
      assertEquals("324234@qq.com", userDto0.getEmail());
      assertEquals("u738Bu4E94", userDto0.getUsername());
      assertEquals(2, (int)userDto0.getId());
  }

  @Test(timeout = 4000)
  public void test6()  throws Throwable  {
      Integer integer0 = new Integer(1);
      UserServiceImpl userServiceImpl0 = new UserServiceImpl();
      UserDto userDto0 = userServiceImpl0.getUserById(integer0);
      assertEquals(18, (int)userDto0.getAge());
      assertEquals("u5F20u4E09", userDto0.getUsername());
      assertEquals("324234@qq.com", userDto0.getEmail());
      assertEquals(1, (int)userDto0.getId());
  }

  @Test(timeout = 4000)
  public void test7()  throws Throwable  {
      UserServiceImpl userServiceImpl0 = new UserServiceImpl();
      Integer integer0 = new Integer(1);
      // Undeclared exception!
      try { 
        userServiceImpl0.getCourseById(integer0);
        fail("Expecting exception: NullPointerException");
      
      } catch(NullPointerException e) {
         //
         // no message in exception (getMessage() returned null)
         //
         verifyException("com.netease.evo.service.impl.UserServiceImpl", e);
      }
  }
}

通过我们生成的单测,我们可以发现一下几点:
如果参数作为分支条件,EvoSuite能很好的生成分支覆盖用例,测试用例数没有特别多,采用了遗传算法取最小集
每个用例类生成一个用例基类,用于在开始测试前初始化 evosuite 的沙盒机制,保证用户执行的安全
依赖对象没有处理,也没办法和spring集成使用
除了参数作为判断的情况外,没有校验正常逻辑,都是校验null逻辑进行判断
生成比较耗时,一个类的测试用例生成需要2分钟左右
原理
evosuite主要采用了遗传搜索算法,由于相关文档偏少,了解到evosuite首先使用ASM工具将被测类的class进行遍历,分析里面的代码行(evosuite拥有多充策略生成测试用例,如分支覆盖,行覆盖等),然后采用生成随机用例传递给遗传分析算法,进行用例筛选,筛选主要通过适应度函数,理论上分析时间越长,越能找到合适的用例。

diffblue cover 介绍

Diffblue Cover是一种自动化的单元测试编写工具。它分析您现有的Java应用程序,并编写反映当前行为的单元测试,从而增加测试范围并帮助您在将来的代码更改中查找回归。Cover在代码更改时通过更新测试来自动维护测试。Cover支持标准Java 8和11,Spring和Spring Boot。
Cover可以作为Windows和Linux的CLI工具使用,并且可以在您的Maven或Gradle环境中自行配置,可以100%自主运行。还有用于IntelliJ IDEA的Cover插件,用于交互式测试编写。
由VERTIV赞助
总部位于英国牛津大学的初创公司Diffblue对此深信不疑。周二,Diffblue宣 布其旗舰产品Diffblue Cover和Diffblue Cover: Community Edition全面上市,这是使用IntelliJ(最流行的企业Java IDE)为开发人员创建的免费版
执行步骤
它首先确保您的代码已编译。这是必需的,因为它对字节码执行分析。
然后它通过要测试的每个方法的路径进行尝试生成测试文件。它使用的输入与开发人员的输入相似。同时,它模拟了测试所依赖的文件
最后,它找出可能有用的断言并将它们添加到测试中。请注意,断言始终反映代码的当前行为。
它不做的是猜测您的代码的预期行为。
有时它可能找不到适合您的代码的测试,在这种情况下,它可能会返回一个部分测试供您完成。

安装插件
在这里插入图片描述

运行插件
在这里插入图片描述

package com.netease.evo.service.impl;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.netease.evo.dto.CourseDto;
import com.netease.evo.dto.UserDto;
import com.netease.evo.dto.UserQueryDto;
import com.netease.evo.service.CourseService;

import java.util.ArrayList;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ContextConfiguration(classes = {UserServiceImpl.class})
@ExtendWith(SpringExtension.class)
class UserServiceImplTest {
    @MockBean
    private CourseService courseService;

    @Autowired
    private UserServiceImpl userServiceImpl;

    @Test
    void testGetUserById() {
        UserDto actualUserById = this.userServiceImpl.getUserById(1);
        assertEquals(18, actualUserById.getAge().intValue());
        assertEquals("张三", actualUserById.getUsername());
        assertNull(actualUserById.getType());
        assertEquals(1, actualUserById.getId().intValue());
        assertEquals("324234@qq.com", actualUserById.getEmail());
    }

    @Test
    void testGetUserById2() {
        assertNull(this.userServiceImpl.getUserById(123));
    }

    @Test
    void testGetUserById3() {
        UserDto actualUserById = this.userServiceImpl.getUserById(2);
        assertEquals(19, actualUserById.getAge().intValue());
        assertEquals("王五", actualUserById.getUsername());
        assertNull(actualUserById.getType());
        assertEquals(2, actualUserById.getId().intValue());
        assertEquals("324234@qq.com", actualUserById.getEmail());
    }

    @Test
    void testQueryUser() {
        assertEquals(1, this.userServiceImpl.queryUser(new UserQueryDto("janedoe", 1)).size());
        assertEquals(1, this.userServiceImpl.queryUser(new UserQueryDto("janedoe", 2)).size());
    }

    @Test
    void testGetCourseById() {
        when(this.courseService.getByUserId((Integer) any())).thenReturn(new ArrayList());
        assertTrue(this.userServiceImpl.getCourseById(1).isEmpty());
        verify(this.courseService).getByUserId((Integer) any());
    }

    @Test
    void testGetCourseById2() {
        ArrayList courseDtoList = new ArrayList();
        courseDtoList.add(new CourseDto());
        when(this.courseService.getByUserId((Integer) any())).thenReturn(courseDtoList);
        assertTrue(this.userServiceImpl.getCourseById(1).isEmpty());
        verify(this.courseService).getByUserId((Integer) any());
    }

    @Test
    void testGetCourseById3() {
        ArrayList courseDtoList = new ArrayList();
        courseDtoList.add(new CourseDto());
        courseDtoList.add(new CourseDto());
        when(this.courseService.getByUserId((Integer) any())).thenReturn(courseDtoList);
        assertTrue(this.userServiceImpl.getCourseById(1).isEmpty());
        verify(this.courseService).getByUserId((Integer) any());
    }
}

通过我们生成的单测,我们可以发现一下几点:
如果参数作为分支条件,diffblue同样能很好的生成分支覆盖用例,测试用例数没有特别多,采用了AI算法取最小集
用例使用springboot2.x,很好的兼容了springboot,注入了被测类
被测类的依赖环境使用了大量mock保证代码顺利运行
生成比较耗时,一个类的测试用例生成需要2分钟左右

Randoop介绍

randoop是一个为JAVA单元测试生成测试用例的框架(生成器),它基于Junit格式为编译后JAVA字节码(classes)自动生成测试用例,目前只有命令行方式的支持。
Randoop通过反馈式的随机测试来生成测试用例,由于测试数据的随机性,随机测试往往很难有较高的覆盖率。
使用方法
 下载Randoop的jar包,如randoop-4.2.6.zip,解压取出randoop-all-4.2.6.jar,将randoop-all-4.2.6.jar放入被测类编译后的文件目录,然后执行:

java -Xmx3000m -classpath  randoop-all-4.2.6.jar randoop.main.Main  gentests --testclass=com.netease.evo.service.impl.UserServiceImpl --time-limit=60

查看生成的测试类,发现randoop为被测类的每个方法生成了将近500个测试用例,但是没有对被测方法的语义和逻辑进行分析,可以看出测试用例基本没有意义

@Test
public void test0499() throws Throwable {
    if (debug)
        System.out.format("%n%s%n", "RegressionTest0.test0499");
    com.netease.evo.service.impl.UserServiceImpl userServiceImpl0 = new com.netease.evo.service.impl.UserServiceImpl();
    com.netease.evo.dto.UserDto userDto2 = userServiceImpl0.getUserById((java.lang.Integer) 0);
    com.netease.evo.dto.UserDto userDto4 = userServiceImpl0.getUserById((java.lang.Integer) 1);
    com.netease.evo.dto.UserDto userDto6 = userServiceImpl0.getUserById((java.lang.Integer) (-1));
    com.netease.evo.dto.UserDto userDto8 = userServiceImpl0.getUserById((java.lang.Integer) 100);
    com.netease.evo.dto.UserDto userDto10 = userServiceImpl0.getUserById((java.lang.Integer) 10);
    com.netease.evo.dto.UserDto userDto12 = userServiceImpl0.getUserById((java.lang.Integer) 0);
    com.netease.evo.dto.UserDto userDto14 = userServiceImpl0.getUserById((java.lang.Integer) 100);
    com.netease.evo.dto.UserQueryDto userQueryDto15 = null;
    // The following exception was thrown during execution in test generation
    try {
        java.util.List userDtoList16 = userServiceImpl0.queryUser(userQueryDto15);
        org.junit.Assert.fail("Expected exception of type java.lang.NullPointerException; message: null");
    } catch (java.lang.NullPointerException e) {
        // Expected exception.
    }
    org.junit.Assert.assertNull(userDto2);
    org.junit.Assert.assertNotNull(userDto4);
    org.junit.Assert.assertNull(userDto6);
    org.junit.Assert.assertNull(userDto8);
    org.junit.Assert.assertNull(userDto10);
    org.junit.Assert.assertNull(userDto12);
    org.junit.Assert.assertNull(userDto14);
}

@Test
public void test0500() throws Throwable {
    if (debug)
        System.out.format("%n%s%n", "RegressionTest0.test0500");
    com.netease.evo.service.impl.UserServiceImpl userServiceImpl0 = new com.netease.evo.service.impl.UserServiceImpl();
    com.netease.evo.dto.UserDto userDto2 = userServiceImpl0.getUserById((java.lang.Integer) 0);
    com.netease.evo.dto.UserDto userDto4 = userServiceImpl0.getUserById((java.lang.Integer) 10);
    com.netease.evo.dto.UserDto userDto6 = userServiceImpl0.getUserById((java.lang.Integer) 100);
    com.netease.evo.dto.UserDto userDto8 = userServiceImpl0.getUserById((java.lang.Integer) 1);
    java.lang.Class wildcardClass9 = userServiceImpl0.getClass();
    org.junit.Assert.assertNull(userDto2);
    org.junit.Assert.assertNull(userDto4);
    org.junit.Assert.assertNull(userDto6);
    org.junit.Assert.assertNotNull(userDto8);
    org.junit.Assert.assertNotNull(wildcardClass9);
}

总结

randoop只支持命令行方式,每次生成用例需要手动编写用例行,生成的用例意义也不是特别大,squaretest使用方便了,能帮我们编写掉大部分用例代码,但是具体的用例参数需要我们手动填充,可以说是解放了大半的单测编写时间,但是该插件收费,只比IDEA自带的单测生成工具多了方法内容填充
Diffblue和 EvoSuite则是在squaretest的更近一步,能够推导用例参数,但是生成时间较长,部分用例参数依旧有部分需要手工去维护,当然没有一款完全的自动化单测编写工具,只有更加智能的单测编写工具。
Diffblue对比EvoSuite,我们惊奇的发现被测类同样生成了7个测试方法,大部分测试方法用例相同(我们推测Diffblue也是采用了遗传搜索算法,但是没有相关资料显示这个结果),但是Diffblue用例更加凝聚,没有多余的测试代码,代码可读性更高,但Diffblue分为社区版和商业版,没有进行开源,无法进行二次开发,如果是工作需求可以采用Diffblue更方便,如果做产品可以基于EvoSuite进行二次开发兼容各种客户端和环境