单元测试
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
以上是关于单元测试的主要内容,如果未能解决你的问题,请参考以下文章