单元测试

Posted vole

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单元测试相关的知识,希望对你有一定的参考价值。

一、什么是单元测试

单元测试并不只是为了验证你当前所写的代码是否存在问题,更为重要的是它可以很大程度的保障日后因业务变更、修复Bug或重构等引起的代码变更而导致(或新增)的风险。

同时将单元测试提前到编写正式代码进行(测试驱动开发),可以很好的提高对代码结构的设计。通过优先编写测试用例,可以很好的从用户角度来对功能的分解、使用过程和接口等进行设计,从而提高代码结构的高内聚、低耦合特性。使得对日后的需求变更或代码重构等更加高效、简洁。

因此编写单元测试对产品开发和维护、技术提升和积累具有重大意义!

二、为什么要进行单元测试

1、进行开发时,一定要注意代码的高内聚,低耦合性,这样也有助于单元测试的编写。

2、一个功能进行单元测试可以分为三个部分:准备数据,调用方法,验证。(展开详细说)

3、一个方法如果融合了很多的逻辑,就没有办法对某一段逻辑进行测试。这就对代码的内聚性有较高的要求。

4、一个接口的返回值,可能对我们对功能的验证并不友好,这就要求我们对这个接口内的方法进行具体的单元测试编写:

高内聚很容易做到这一点:我们可以针对这个接口功能内部的方法进行覆盖测试:不妨以这三个步骤进行测试:【1】对数据处理逻辑尽行单元测试;【2】对逻辑进行单元测试;【3】对调用的工具类进行单元测试(需要注意的是,封装而成的工具类一般都有一定的成熟度,不需要在进行单元测试的编写了)。

这样,虽然这个接口的整体返回值并不友好,我们却通过对该接口内部的每个方法,保证了功能的正确性。

其实,排除了工具类的测试,主要是针对数据处理逻辑的验证。

三、单元测试代码示例

1、Junit4 

1.1引入依赖

<properties>
  <spring.version>4.3.5.RELEASE</spring.version>
</properties>

<!--Junit4 start-->
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-mock</artifactId>
    <version>2.0.8</version><!-- 2.0 -->
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
 <groupId>framework</groupId>
 <artifactId>pdfb-framework</artifactId>
 <version>1.0.0</version>
</dependency>
  <dependency>
    <groupId>mockito-core</groupId>
    <artifactId>mockito-core</artifactId>
    <version>1.10.19</version>
  </dependency>
  <!--<dependency>
    <groupId>objenesis</groupId>
    <artifactId>objenesis</artifactId>
    <version>3.0.1</version>
  </dependency>-->
  <dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.0-beta.5</version>
  </dependency>
  <dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito</artifactId>
    <version>1.6.5</version>
  </dependency>
  <dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito-common</artifactId>
    <version>1.6.5</version>
  </dependency>

  <dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-support</artifactId>
    <version>2.0.0-beta.5</version>
  </dependency>

  <dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-core</artifactId>
    <version>2.0.0-beta.5</version>
  </dependency>

  <dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4-common</artifactId>
    <version>2.0.0-beta.5</version>
  </dependency>

  <dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-reflect</artifactId>
    <version>2.0.0-beta.5</version>
  </dependency>
  <!--Junit4 end-->

1.2 样例一:访问数据库

本样例是应用的ssm框架,需要添加
@RunWith(SpringJUnit4ClassRunner.class)的注解,才能调用Spring容器。
注解
@ContextConfiguration(locations = "classpath:spring/applicationContext.xml")
的作用是访问Spring的配置文件。
package com.asd.modules.service.impl;

import com.asd.common.shiro.util.StringUtils;
import com.asd.common.utils.TestUtils;
import com.asd.common.utils.time.DateUtils;
import com.asd.modules.dao.IBNRExportMapper;
import com.asd.modules.dao.TAssessTreatyMapper;
import com.asd.modules.pojo.currencybreak.model.CurrencyBreak;
import com.asd.modules.service.CommonService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.math.BigDecimal;
import java.util.*;

/**
 * @author zs
 * @date 2019/5/26 9:57
 * @Description 测试偿付能力外币准备金拆分结果全部导出功能
 */

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring/applicationContext.xml")
public class CurrencyBreakServiceImplTest {

    @Autowired
    private IBNRExportMapper ibnrExportMapper;

    @Autowired
    private TAssessTreatyMapper tAssessTreatyMapper;

    @Autowired
    private CommonService commonService;

    CurrencyBreakServiceImpl currencyBreakServiceImpl = new CurrencyBreakServiceImpl();

    /**
     *@Author: zs on 2019/5/26 10:02
     *@param: []
     *@return: void
     *@Description:测试比例合约未到期--毛数据
     */
    @Test
    public void testTAssessTreatyMapper() throws Exception {

        //创建比例合约未到期--毛数据集合用于验证查询结果
        Map<String,BigDecimal> assessDate_true = new HashMap<>();
        assessDate_true.put("HKD",new BigDecimal(0.8523100000+9275116.801640+10882327.793455));
        assessDate_true.put("AUD",new BigDecimal(4.7802000000+88988.727791+18616.109742));
        assessDate_true.put("VND",new BigDecimal(0.0002884000+106447.623658+369097169.410541));
        assessDate_true.put("JPY",new BigDecimal(0.0603210000+495999.744668+8222671.120638));
        assessDate_true.put("DKK",new BigDecimal(1.0199000000+21.208229+20.794420));
        assessDate_true.put("XOF",new BigDecimal(0.0116000000+1353.861510+116712.199138));
        assessDate_true.put("CAD",new BigDecimal(5.0882000000+375383.884451+73775.379201));
        assessDate_true.put("CHF",new BigDecimal(6.6841000000+1493.033675+223.370936));
        assessDate_true.put("EUR",new BigDecimal(7.6082000000+10727578.819760+1410002.210741));
        assessDate_true.put("LAK",new BigDecimal(0.0007795000+58.003429+74411.069917));
        assessDate_true.put("USD",new BigDecimal(6.6901000000+163962755.269204+24508266.732815));
        assessDate_true.put("MOP",new BigDecimal(0.8276000000+649.709801+785.052925));
        assessDate_true.put("GBP",new BigDecimal(8.9057000000+18424172.235436+2068806.745729));

        //查询未到期数据
        Date assessdate = DateUtils.strToDate("2019-03-31");
        List<CurrencyBreak> currencyBreakList_base = tAssessTreatyMapper.searchDataByCurrency(assessdate, false, null, null, null, null);
        //比较数据是否一致
        Assert.assertEquals(assessDate_true.size(),currencyBreakList_base.size());
        //遍历数据
        for (CurrencyBreak currency: currencyBreakList_base) {
            //得到正确的值
            BigDecimal date_true = assessDate_true.get(currency.getCurrency()).setScale(6,BigDecimal.ROUND_HALF_UP);
            //得到查询的值
            BigDecimal date_base = currency.getExchrate().add(currency.getAssessCurrency()).add(currency.getAssessAccCurrency()).setScale(6,BigDecimal.ROUND_HALF_UP);
            //比较元素是否一样
            Assert.assertEquals(currency.getCurrency() + ":" + date_true,currency.getCurrency() + ":" + date_base);
        }

    }
}

1.3 样例2:利用mock工具,模拟测试数据,并不访问数据库。

需要注意的是:由于service类没有get()与set()方法,这里需要在测试前,对serviceimpl的Mapper属性赋值。

测试用工具类

//测试用公共方法
package com.asd.common.utils;

import org.apache.poi.ss.usermodel.Workbook;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;

public class TestUtils {

    /**
     * 给controllerTest类setservice属性
     * @param mycontroller
     * @param myService
     * @param serviceName
     */
    public static void setService(Object mycontroller, Object[] myService, String[] serviceName){
        for(int i = 0;i < myService.length; i ++){
            try {
                Field f = mycontroller.getClass().getDeclaredField(serviceName[i]);
                f.setAccessible(true);
                f.set(mycontroller, myService[i]);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    /**
     * 测试私有方法工具
     * @param objectService
     * @param methodName
     * @param objectArg
     * @param objectArgs
     * @return
     * @throws Exception
     */
    public static Object testPrivateMethod(Object objectService, String methodName,Object objectArg, Object... objectArgs) throws Exception {
        // 获取可变参数数组及其长度
        Object[] clone = objectArgs.clone();
        int length = clone.length;
        // 目前以私有方法最多传5个参数处理,count为参数的数量
        int count = 0;
        for(int i = 0; i < 5; i ++){
            if (i == length)
                count = i;
        }
        Class serviceClass = objectService.getClass();
        Class<?> objectArgClass = objectArg.getClass();
        // 获取集合接口class 穷举法
        objectArgClass = getCollectionClass(objectArgClass);

        Method method;
        // 穷举出所有可变参数class
        switch (count){
            case 0 :
                // 通过反射获取当前service私有方法
                method = serviceClass.getDeclaredMethod(methodName, objectArgClass);
                // 设置私有属性可访问
                method.setAccessible(true);
                // 反射执行方法
                return method.invoke(objectService,objectArg);
            case 1 :
                Class<?> argsClass1 = clone[0].getClass();
                argsClass1 = getCollectionClass(argsClass1);
                method = serviceClass.getDeclaredMethod(methodName, objectArgClass, argsClass1);
                method.setAccessible(true);
                return method.invoke(objectService,objectArg,clone[0]);
            case 2 :
                Class<?> argsClass11 = clone[0].getClass();
                argsClass11 = getCollectionClass(argsClass11);
                Class<?> argsClass2 = clone[1].getClass();
                argsClass2 = getCollectionClass(argsClass2);
                method = serviceClass.getDeclaredMethod(methodName, objectArgClass, argsClass11,argsClass2);
                method.setAccessible(true);
                return method.invoke(objectService,objectArg,clone[0],clone[1]);
            case 3 :
                Class<?> argsClass111 = clone[0].getClass();
                argsClass111 = getCollectionClass(argsClass111);
                Class<?> argsClass22 = clone[1].getClass();
                argsClass22 = getCollectionClass(argsClass22);
                Class<?> argsClass3 = clone[2].getClass();
                argsClass3 = getCollectionClass(argsClass3);
                method = serviceClass.getDeclaredMethod(methodName, objectArgClass, argsClass111,argsClass22,argsClass3);
                method.setAccessible(true);
                return method.invoke(objectService,objectArg,clone[0],clone[1],clone[2]);
            case 4 :
                Class<?> argsClass1111 = clone[0].getClass();
                argsClass1111 = getCollectionClass(argsClass1111);
                Class<?> argsClass222 = clone[1].getClass();
                argsClass222 = getCollectionClass(argsClass222);
                Class<?> argsClass33 = clone[2].getClass();
                argsClass33 = getCollectionClass(argsClass33);
                Class<?> argsClass4 = clone[3].getClass();
                argsClass4 = getCollectionClass(argsClass4);
                method = serviceClass.getDeclaredMethod(methodName, objectArgClass, argsClass1111,argsClass222,argsClass33,argsClass4);
                method.setAccessible(true);
                return method.invoke(objectService,objectArg,clone[0],clone[1],clone[2],clone[3]);
        }
        return null;
    }

    // 获取集合定义class
    public static Class<?> getCollectionClass(Class<?> objectArgClass) {
        String className = objectArgClass.getName();
        switch (className) {
            case "java.util.ArrayList" :
                objectArgClass = List.class;
                break;
            case "java.util.HashMap" :
                objectArgClass = Map.class;
                break;
            case "org.apache.poi.hssf.usermodel.HSSFWorkbook" :
                objectArgClass = Workbook.class;
                break;
            case "org.apache.poi.xssf.usermodel.XSSFWorkbook" :
                objectArgClass = Workbook.class;
                break;
        }
        return objectArgClass;
    }
}

测试样例

//测试样例
package com.asd.modules.service.impl;

import com.asd.common.utils.TestUtils;
import com.asd.common.utils.time.DateUtils;
import com.asd.modules.dao.*;
import com.asd.modules.pojo.currencybreak.model.CurrencyBreak;
import com.asd.modules.service.CommonService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.math.BigDecimal;
import java.util.*;

import static org.mockito.Mockito.when;

/**
 * @author zs
 * @date 2019/5/27 11:04
 */
public class CurrencyBreakServiceImplMockTest {

    @Rule
    public MockitoRule rule = MockitoJUnit.rule();
    @Mock private TAssessTreatyMapper tAssessTreatyMapper;
    @Mock private TAssessSuptreatyMapper tAssessSuptreatyMapper;
    @Mock private TAssessFacMapper tAssessFacMapper;
    @Mock private TAssessSupfacMapper tAssessSupfacMapper;
    @Mock private IBNRExportMapper ibnrExportMapper;
    @Mock private CommonService commonService;
    CurrencyBreakServiceImpl currencyBreakService = new CurrencyBreakServiceImpl();

    @Before
    public void test(){
        Object[] serviceArr = {tAssessTreatyMapper,tAssessSuptreatyMapper,tAssessFacMapper,tAssessSupfacMapper,ibnrExportMapper,commonService};
        String[] stringArr = {"tAssessTreatyMapper","tAssessSuptreatyMapper","tAssessFacMapper","tAssessSupfacMapper","ibnrExportMapper","commonService"};
        TestUtils.setService(currencyBreakService, serviceArr,stringArr);
    }

    @Test
    public void testGenIPCurrencyBreak(){
        //数据库:未到期:6.6901000000 163962755.269204   24508266.732815  6.7286000000  164906323.538419
        //数据库:未决:6.6901000000  383428556.653315   57312828.904398  6.7286000000  385635100.566132
        //查询数据库中的数据
        Date date = DateUtils.strToDate("2019-03-31");
        //创建正确数据集
        List<CurrencyBreak> currencyBreak_assessList_true = new ArrayList<>();
        List<CurrencyBreak> currencyBreak_outstandingList_true = new ArrayList<>();
        //未到期数据
        CurrencyBreak currencyBreak_assess_true = new CurrencyBreak();
        currencyBreak_assess_true.setCurrency("USD");
        currencyBreak_assess_true.setExchrate(new BigDecimal(6.6901000000));
        currencyBreak_assess_true.setAssessCurrency(new BigDecimal(163962755.269204));
        currencyBreak_assess_true.setAssessAccCurrency(new BigDecimal(24508266.732815));
        currencyBreak_assessList_true.add(currencyBreak_assess_true);
        //未决数据
        CurrencyBreak currencyBreak_outstanding_true = new CurrencyBreak();
        currencyBreak_outstanding_true.setCurrency("USD");
        //currencyBreak_outstanding_true.setExchrate(new BigDecimal(6.6901000000));
        currencyBreak_outstanding_true.setOutstandingCurrency(new BigDecimal(383428556.653315));
        //currencyBreak_outstanding_true.setOutstandingAcccurrency(new BigDecimal(57312828.904398));
        currencyBreak_outstandingList_true.add(currencyBreak_outstanding_true);
        //评估时点汇率
        //当前时点汇率
        Map<String, BigDecimal> assessdateExchrate = new HashMap<>();
        assessdateExchrate.put("USD",new BigDecimal(6.6901000000));
        //当前时点汇率
        Set<String> acccurrencykey = assessdateExchrate.keySet();
        Map<String, BigDecimal> newdateExchrate = new HashMap<>();
        newdateExchrate.put("USD",new BigDecimal(6.7286000000));
        when(tAssessTreatyMapper.searchDataByCurrency(date,false,null,null,null,null)).thenReturn(currencyBreak_assessList_true);
        when(ibnrExportMapper.searchDataByCurrency(date,false,null,null,null,null)).thenReturn(currencyBreak_outstandingList_true);
        when(commonService.getAssessdateExchrate(date)).thenReturn(assessdateExchrate);
        when(commonService.getNewExchrate(acccurrencykey)).thenReturn(newdateExchrate);
        List<CurrencyBreak> currencyBreakList_base = currencyBreakService.genIPCurrencyBreak(date, false, null, null, null, null);
        //比较USD的数据
        for (CurrencyBreak currencyBreak_base : currencyBreakList_base) {
            if ("USD".equals(currencyBreak_base.getCurrency())) {
                Assert.assertEquals(new BigDecimal(164906323.538419).setScale(6,BigDecimal.ROUND_HALF_UP),currencyBreak_base.getAssessNewCurrency());
                Assert.assertEquals(new BigDecimal(385635100.566132).setScale(6,BigDecimal.ROUND_HALF_UP),currencyBreak_base.getOutstandingNewCurrency());
            }
        }
    }
}

2、TestNG

2.1引入依赖

<!--TestNG begin-->
    <!--TestNG-->
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>6.14.3</version>
      <scope>test</scope>
    </dependency>
    <!--JMockit-->
    <dependency>
      <groupId>org.jmockit</groupId>
      <artifactId>jmockit</artifactId>
      <version>1.46</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.jmockit</groupId>
      <artifactId>jmockit-coverage</artifactId>
      <version>1.23</version>
      <scope>test</scope>
    </dependency>
    <!--Spring Test-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${spring.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.kubek2k</groupId>
      <artifactId>springockito</artifactId>
      <version>1.0.9</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.kubek2k</groupId>
      <artifactId>springockito-annotations</artifactId>
      <version>1.0.9</version>
      <scope>test</scope>
    </dependency>
      <!--<dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-servlet-api</artifactId>
        <version>${tomcat.servlet.api.version}</version>
        <scope>test</scope>
      </dependency>-->

    <!--TestNG end-->

2.2、样例1:访问数据库。

package com.germa.service;

import com.germa.domain.Forum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests;
import org.testng.annotations.Test;


@ContextConfiguration("classpath:applicationContext-mybatis.xml")
@Rollback(false)
public class TestMyBatis extends
        AbstractTransactionalTestNGSpringContextTests {

    private ForumService forumService;
    @Autowired
    public void setForumService(ForumService forumService) {
        this.forumService = forumService;
    }

    @Test
    public void testAddForum(){
        //ApplicationContext ctx=new ClassPathXmlApplicationContext("classpath:applicationContext-mybatis.xml");
       // this.forumService=(ForumService)ctx.getBean("forumService");
        Forum forum=new Forum();
        forum.setForumId(3);
        forum.setForumName("forum3");
        forum.setForumDesc("Desc3");
        this.forumService.addForum(forum);
    }
    
}

注意:

  • 第一个坑:在一开始测试时我直接新建一个测试类,并给类注解@ContextConfiguration("classpath:applicationContext-mybatis.xml"),想通过注解自动加载配置文件,然后在直接在想要测试的方法前加了testng的@Test,一运行,报错NullPointerException,然后我试了在方法内部用ApplicationContext手动加载,结果就可以了,发现是想要的Bean没有被自动注入,网上查了一下,原来在测试类想要融入Spring环境下(注解自动加载配置文件),必须extends 一个叫AbstractTransactionalTestNGSpringContextTests的类,此时,问题得以解决。
  • 第二个坑:方法成功运行,结果却在数据库看不到相应的结果,困顿许久,看了下测试产生的信息,原来是继承AbstractTransactionalTestNGSpringContextTests在测试时会自动启动事务,自动回滚,这时,只需要给测试类注解@RollBack(false)即可。

2.32、样例2:利用mock工具进行testNG测试还没有验证成功,待更新。

参看链接:

https://blog.csdn.net/ifidieyoung/article/details/83615394

技术图片

以上是关于单元测试的主要内容,如果未能解决你的问题,请参考以下文章

四则运算单元测试

单元测试很棒,但是

词频统计单元测试

第1129期对vue.js单文件(.vue)进行单元测试

为啥我必须切换纹理单元才能让我的片段着色器识别要使用的纹理?

junit4单元测试--web项目中模拟登录会话,做全流程测试