java单元测试实战

单元测试

单元测试的优点

  1. 验证行为
  2. 设计行为
  3. 编写文档
  4. 回归行为

单元测试使用的工具 for java

测试准备

引入pom.xml

<!-- ======= -->
<!-- 测试配置版本 -->
<!-- ======= -->

<properties>
<mockito.version>3.8.0</mockito.version>
<powermock.version>2.0.9</powermock.version>
</properties>

<!-- ======= -->
<!-- 测试依赖 -->
<!-- ======= -->
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-inline</artifactId>
      <version>${mockito.version}</version>
      <scope>test</scope>
    </dependency>
    
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>${mockito.version}</version>
      <scope>test</scope>
    </dependency>
    
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-junit4</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-module-junit4-common</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-api-mockito2</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <artifactId>mockito-core</artifactId>
          <groupId>org.mockito</groupId>
        </exclusion>
      </exclusions>
    </dependency>

    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-core</artifactId>
      <version>${powermock.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-test</artifactId>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>

    <!--springboot程序测试依赖,如果是自动创建项目默认添加-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>commons-beanutils</groupId>
      <artifactId>commons-beanutils</artifactId>
      <version>1.9.4</version>
    </dependency>

<!-- ======= -->
<!-- jacoco插件 -->
<!-- ======= -->
 <build>
    <finalName>demo</finalName>
    <plugins>
      <plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>0.7.8</version>
        <executions>
          <execution>
            <goals>
              <goal>prepare-agent</goal>
              <goal>report</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      </plugins>
      </build>

测试配置准备

在代码里我们时常会有一些配置类需要提前加载,我们可以使用@TestConfiguration 注解 来配置一些配置类的bean
我们使用的rpc 服务也可以在这边进行注解生成 进行mock
例如

/**
相关配置类
*/
@TestConfiguration
public class PeiZhiTestConfig {
  //配置一
  @MockBean
  private PeiZhiOneConfig peiZhiOneConfig;
  
  //rpc服务的代理类
  @MockBean
  private RemoteRpcAgent remoteRpcAgent;

  @Before
  public void before() {
    MockitoAnnotations.openMocks(PeiZhiOneConfig.class);
    MockitoAnnotations.openMocks(remoteRpcAgent.class);
  }
}

测试环境集成

controller 层 基础类

//以spring方式启动
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK, classes = ApplicationStarter.class)
//自动创建一个mvcmock上下文
@AutoConfigureMockMvc
//导入配置
@Import(value = { PeiZhiTestConfig.class})
//测试使用配置文件
@TestPropertySource(locations = {"classpath:application-dev.properties"})
// 启用环境名
@ActiveProfiles(profiles = "dev")
//测试使用事务以免污染数据
@Transactional
// 将这些类交给系统的类加载器加载.而不是powermock
@PowerMockIgnore({"org.apache.*", "javax.xml.*", "org.xml.sax.*", "org.w3c.dom.*", "org.apache.log4j.*", "org.slf4j.*", "javax.*", "java.lang.*", "com.sun.*"})
public class BaseJunitTest {
  @Test
  public void testInit() {
    System.out.println("基础测试");
  }
}

service 层

//以powerMockRunner启动
@RunWith(PowerMockRunner.class)
//powermock代理掉springjunit4 用于生成其他
@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)
//忽略掉以下包, 交给springjunitrunner执行
@PowerMockIgnore({"org.apache.*", "javax.xml.*", "org.xml.sax.*", "org.w3c.dom.*",
    "org.apache.log4j.*", "org.slf4j.*", "javax.*", "java.lang.*", "com.sun.*"})
public class BaseJunitTest {
  @Test
  public void testInit() {
    System.out.println("基础测试");
  }
}

单元测试实例代码

RemoteRpcAgent代码
用于代理RPC接口

@Slf4j
@Component
public class RemoteRpcAgent{

    @Reference( version = "1.0")
    private TestDubboService testDubboService;

    /**
    * 调用 doSomething     RPC  
     */
    public BaseResponse<Boolean> doSomething(String test){
        BaseResponse<Boolean> response;
        try {
            response = testDubboService.doSomething(ids);
            if (Objects.isNull(response) || !response.isSuccess()) {
                log.warn(LogUtils.msg2LogMessage("doSomething_fail||ids={}", JsonUtils.toJson(ids)).toString());
            }
        }catch (Exception e){log.error(LogUtils.msg2LogMessage(e.getMessage()).toString(), e);
            e.printStackTrace();
            throw new BizException("dosomething异常");
        }
        return response;
    }

}

具体的测试类

public class ConstructTest extends BaseJunitTest {
  //如果想调用Contronller 层的方法使用injectMocks 
  @InjectMocks
  private TestController testController;
  
  //想生成bean 方法用
   @MockBean
  private TestCall testCall;
  
  @Resource
  private MockMvc mockMvc;

  @Resource
  private RemoteRpcAgent remoteRpcAgent;
  //加载桩
  @Before
  public void setUpData() {
    System.out.println("before");
//意思,当调用remoteRpcAgent类的doSomething方法 的时候,传入any() 任何参数 ,都返回BaseResponse.true 这个结果
when(remoteRpcAgent.doSomething(any())).thenReturn(BaseResponse.build(true));
//丢出RuntimeException 当TestRepository执行testJob()方法的时候
  doThrow(RuntimeException.class).when(TestRepository).testJob();
  }
  
//期望获取异常
 @Test(expected = TestException.class)
  public void test_exceptedException() {
    TestRepository.testJob();
  }

  /**
   * 测试接口
   */
  @Test
  @SqlGroup(value = {
      @Sql(scripts = {
          "/test.sql",
         })
  })
  public void test_testurl() throws Exception {
  	String jsonString="{\"name\":\"测试\"};
    MvcResult mvcResult = mockMvc.perform(
    //post 方法 /get方法
        MockMvcRequestBuilders.post("/testurl")
        //路径参数
            .param("param1", "0")
            //头参数
            .header("header1", "0")
            //发送请求类型
            .contentType(MediaType.APPLICATION_JSON_UTF8)
             //期望获取结果类型
            .accept(MediaType.APPLICATION_JSON))
            //body参数
            .content(jsonString)
            //期望获取结果
        .andExpect(MockMvcResultMatchers.status().isOk())
        .andDo(MockMvcResultHandlers.print())
        .andReturn();
    // 得到返回代码
    int status = mvcResult.getResponse().getStatus();
    // 得到返回结果
    String content = mvcResult.getResponse().getContentAsString();
    Assert.assertEquals(200, status);

   }
}

单元测试实体类/枚举等

我们想要提高单元测试覆盖率会发现还有大量的实体类没有走过,由其是当使用lomcok 插件生成的, 简直惨不忍睹所以提供实体类测试工具
主要思想是通过反射获取类/方法/参数 进行生成测试
实体类工具

public class EntityTestUtils {

  //实体化数据
  private static final Map<String, Object> STATIC_MAP = new HashMap<String, Object>();

  //忽略的函数方法method
  private static final String NO_NOTICE = "getClass,notify,notifyAll,wait,clone";

  static {
    STATIC_MAP.put("java.lang.Long", 1L);
    STATIC_MAP.put("java.lang.String", "test");
    STATIC_MAP.put("java.lang.Integer", 1);
    STATIC_MAP.put("int", 1);
    STATIC_MAP.put("long", 1);
    STATIC_MAP.put("java.util.Date", new Date());
    STATIC_MAP.put("char", '1');
    STATIC_MAP.put("java.util.Map", new HashMap());
    STATIC_MAP.put("boolean", true);
  }


  /**
   * @param CLASS_LIST 类列表
   * @throws IllegalAccessException
   * @throws InvocationTargetException
   * @throws InstantiationException
   */
  public static void runTest(List<Class> CLASS_LIST)
      throws IllegalAccessException, InvocationTargetException, InstantiationException, CloneNotSupportedException, NoSuchMethodException {
    for (Class temp : CLASS_LIST) {
      Object tempInstance = new Object();
      //获取声明的内部类
      List<Class> d = Lists.newArrayList(temp.getDeclaredClasses());
      for (Class c :
          d) {
        Object innerInstance = new Object();

        int i = c.getModifiers();
        String s = Modifier.toString(i);
        //获取默认的构造函数
        Constructor con2 = c.getDeclaredConstructors()[0];
        //设置可进入
        con2.setAccessible(true);
        innerInstance = con2.newInstance();

        executeMethod(c, innerInstance);
      }

      //执行构造函数
      Constructor[] constructors = temp.getConstructors();
      for (Constructor constructor : constructors) {
        final Class<?>[] parameterTypes = constructor.getParameterTypes();
        if (parameterTypes.length == 0) {
          tempInstance = constructor.newInstance();
        } else {
          Object[] objects = new Object[parameterTypes.length];
          for (int i = 0; i < parameterTypes.length; i++) {
            objects[i] = STATIC_MAP.get(parameterTypes[i].getName());
          }
          tempInstance = constructor.newInstance(objects);
        }
      }

      executeMethod(temp, tempInstance);

    }
  }

/**
* 执行函数中的所有方法
*/
  private static void executeMethod(Class temp, Object tempInstance)
      throws IllegalAccessException, InvocationTargetException {
    //执行函数方法
    Method[] methods = temp.getMethods();
    for (final Method method : methods) {
      if (NO_NOTICE.contains(method.getName())) {
        break;
      }
      final Class<?>[] parameterTypes = method.getParameterTypes();
      if (parameterTypes.length != 0) {
        Object[] objects = new Object[parameterTypes.length];
        for (int i = 0; i < parameterTypes.length; i++) {
          if ("equals".equals(method.getName()) && parameterTypes[i]
              .getName() instanceof java.lang.Object
          ) {
            objects[i] = JsonUtil.clone(tempInstance, temp);
          } else {
            objects[i] = STATIC_MAP.get(parameterTypes[i].getName());
          }
        }
        method.invoke(tempInstance, objects);
      } else {
        method.invoke(tempInstance);
      }
    }

测试类

public class EntityTest extends BaseJunitTest {
  @Test
  public void entityTest()
      throws IllegalAccessException, InstantiationException, InvocationTargetException, CloneNotSupportedException, NoSuchMethodException {
    List<Class> list = new ArrayList<>();
    //这里加要测试的类就行了
    list.add(Resp.class);
    EntityTestUtils.runTest(list);
  }
}

获取测试报告

jacoco 引入后, 会在IDEA的右侧的maven 插件列表中发现jacoco 的插件
我们只要双击install 命令或执行 mvn clean install 则会自动运行生成/target/site/jacoco 目录, 打开jacoco.html 就可以查看你的单元测试覆盖率了

mock 用@value 注入的参数

ReflectionTestUtils.setField(targetObject, "merchantPartnerId", "123");

mybatis-plus

使用mockito报can not find lambda cache for this entity
解决方案

//放到 @BeforeClass 或 @BeforeAll 注解的静态方法内。
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), xxxxDO.class);

参考 github

小技巧

idea 快速生成测试方法 command+N键 →Geneartor → Test → 勾选 set up /tearDown →勾选members →Ok

常见异常

日志包冲突等

相关

jacoco 还可以集成进 sonar 中, 提高代码质量

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值