也谈单元测试
### 一、软件质量与效率的困扰
软件开发是个很精细的工作,同时由于现在互联网的敏捷开发,软件迭代速度非常快,经常每周都会发布,我们要在快速中又保证软件的质量就更不容易。测试就成为了迭代速度的瓶颈。
我们通常的流程是开发然后提交测试人员,而从我们写好代码到得到测试人员的验证反馈,往往要隔几天甚至更长时间,这期间我们写的代码越多,不安全感就越多,因为我们自己没有对自己写的代码有个快速的验证反馈,导致我们提交代码后对自己写的代码心里没底,上线之前一直忐忑不安,严重影响我们的生活质量。
作为程序员,我们要对我们写的每一行代码负责。如何做到写完代码立刻验证得到反馈,在上线前对自己的工作充满信心,发布时自信的睡好觉呢?单元测试,如果我们认真对待单元测试,她总是在那里给我们保驾护航,给我们极大的安全感!
### 二、单元测试的困扰
1、那么多方法是不是每个方法都要写单元测试,会写死人;
单元测试的单元指的是一个方法,但不是所有的方法都要测试。比如下面的代码:
public void setExpress(long orderId, String express,String expressNo) throws Exception{ OrderBaseDto orderBaseDto = orderRedis.getValue(orderId, OrderBaseDto.class); orderBaseDto.setExpress(express); orderBaseDto.setExpressNo(expressNo); orderRedis.setValue(orderId,orderBaseDto); OrderModel updateOrder = new OrderModel(); updateOrder.setOid(orderId); updateOrder.setExpress(express); updateOrder.setExpressNo(expressNo); ordersDao.updateByPrimaryKeySelective(updateOrder); }
这里只有简单的顺序调用,假设要测试的话,究竟是测试什么呢?测试Cpu是否顺序执行?有人说里面有数据库访问,是不是需要测试呢?测试数据库访问测得是什么呢?测试数据库服务是否正常?这似乎是数据库软件商的事情吧?
那么单元测试测什么呢?单元测试是用来测试软件开发工程师自己写的逻辑,如果代码里面没有逻辑就不需要单元测试。所以单元测试代码本身不需要测试,因为单元测试代码都是简单的顺序执行。
2、Mock,单元测试代码跑起来需要模拟上下文,要外部环境模拟;
```java @ApiOperation(value = "超时关单", notes = "超时关单") @GetMapping("/closeOrder") public BaseResponse closeOrder(String companyId,Integer timeout) throws Exception { ListPageByConditionsVo conditionsVo = new ListPageByConditionsVo(); conditionsVo.setOrderStatus(OrderStatus.pendingPayment.getIndex()); conditionsVo.setCompanyId(companyId); Date currentTime = new Date(); Date endDate = DateUtil.addDays(currentTime,1); String startDateStr = DateUtil.convert2String(currentTime,DateUtil.FORMAT_YYYY_MM_DD); String endDateStr = DateUtil.convert2String(endDate,DateUtil.FORMAT_YYYY_MM_DD); conditionsVo.setStartDate(startDateStr); conditionsVo.setEndDate(endDateStr); conditionsVo.setPageSize(1000); List<Long> orderids = orderMag.listIdsByConditionsPage(conditionsVo); if (orderids.size() == 0){ return new BaseResponse("执行成功", orderids.size() ); } orderids.forEach(oid -> { boolean canClose = true; try { logger.info("超时未支付,订单关闭:{}",oid); AbstractOrder order = OrderFactory.getInstance( oid ); //region 判断是否可以关闭订单的逻辑 if (order.getModel().getStatus() != OrderStatus.pendingPayment.getIndex()){ canClose = false; } if ( System.currentTimeMillis() - order.getModel().getGmtCreate().getTime() < timeout * 1000 ){ canClose = false; } //endregion if ( canClose ){ OrdersDto ordersDto = new OrdersDto(); ordersDto.setCancelReason("超时未支付,订单关闭"); order.cancel(ordersDto); } } catch (Exception e) { logger.error("超时未支付,订单关闭 ",e); } }); return new BaseResponse("执行成功", orderids.size()); } ```
这是一个Controller 中的方法,里面有很多判断,这个方法没法跑单元测试比较麻烦,因为要模拟容器上下文,启动服务,这说明我们不该把业务逻辑放在controller层,也提醒我们代码需要重构,这段代码有个关键的逻辑就是判断订单是否可以关闭,我们完全可以把这个逻辑剥离出来写成一个方法:
```java public Boolean canClose(OrdersDto order,Integer timeout){ if (order.getStatus() != OrderStatus.pendingPayment.getIndex()){ return false; } if ( System.currentTimeMillis() - order.getGmtCreate().getTime() < timeout * 1000 ){ return false; } return true; } ```
这样我们用main方法就可以运行这段代码,入参数是订单对象和超时时间 出参是boolean 是否关闭订单,这样我们就可以在单元测试的代码里模拟传入各种可能的参数,对这段逻辑充分的测试,验证它的返回结果是否是我们预期的。单元测试不但保证我们的代码质量,还促使我们提高了代码的结构。
### 三、如何写单元测试
如何测试:
1. 构建输入参数,并预测输入产生的输出结果
2. 调用目标方法,获取输出
3. 检验输出与预期是否一致 (对同一个目标方法构建不同输入,确保各种逻辑可能性都走到,业务逻辑的代码的测试覆盖率应该是100%)
以java 主流的Junit 测试框架为例引入依赖:
```
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
```
Java中maven 约定测试代码都会放在工程的src/test/java路径下,每个要测试的java类对应一个java测试类,例如OrderService 对应的测试类命名为 OrderServiceTest,要测试的方法对应的测试方法名为原方法名前加个test,如:
public class SignTest { public static String public_key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2Tio3W7y2yxefL7HUyTz5m9L/hmse78NFbSeknReYxwOhFUvACWoqbs/DmqAkSNW0NcKaATLLxMK4jtPM7dHMJTmCfxk1E/3ElxNSWIChVF67vxYkf8108OOPsRSx8lQVlvIh/DHPG8gySWyEZtVKEGFOXXYjoUjizD2+/DuC7QIDAQAB"; public static String private_key = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALZOKjdbvLbLF58vsdTJPPmb0v+Gax7vw0VtJ6SdF5jHA6EVS8AJaipuz8OaoCRI1bQ1wpoBMsvEwriO08zt0cwlOYJ/GTUT/cSXE1JYgKFUXru/FiR/zXTw44+xFLHyVBWW8iH8Mc8byDJJbIRm1UoQYU5ddiOhSOLMPb78O4LtAgMBAAECgYBrAoTLS+EBF0N9YFytP5a4GdyHuxpD5Y8kwIblnISOXDtoIXz+c0hLMhJoien4gnxWtLvO9GchrxRxiv0OLIbZpItdV4qjNcetYqMW0/IvlAe9Wijr9e96Zz9EGLoF9dalHF6/Tk53+VC0nXf49BPIW7wKBW0AK2ESMd9NaHFn4QJBAOoj6Tgg/XVxMYevMz2tvisJzfJRDrA2AyVErGXK1k00vYSFQIfjXOkbaQJEvDDRTkBEYX0G/62teW2GOrR2lVUCQQDHU15WS6fZJQNFDTAdLfCzzQK1q2dkVa/kbn4W3lpuczGWcvOxzCUf4lm3nbbJ5EBHoQDAT8yoxEEfaEVEwzc5AkEAwwUUTV8VHgwhQC3K1VXw7rIk6u9e96CVcCZKHiMb6oTCUi4XONhE3Birl2sfAN5lehw6w0PgFI5IdNR38zZOXQJBAJSalw6HQRAnBBULC//1LCsggRCoRWEMcSJBLkgmZg1KXIHqGb1IkbT/sBuwvYIvZa0BX+oAlHiOOG8N8faeBCECQFeuqm7vPrEXcqLwzyIOSffM3wIBMWBtGVQp+/Lp76xB1+l6+Cl9mbzaSl75eHEILSfYHjUsMjq2ZUq2mdkSj70="; @Test public void testSign(){ Map signMap = new HashMap<>(); signMap.put("app_id","10001"); signMap.put("account_type","101"); signMap.put("amount","2000.00"); signMap.put("credit","1"); // signMap.put("ext_param",""); signMap.put("notify_url","http://192.168.31.64:8080/broker/xxxxx"); signMap.put("signtime","1513846074516"); signMap.put("subject","白银店铺"); signMap.put("to_account_type","102"); signMap.put("to_acctId","123"); signMap.put("trade_no","20171221164754467000000000900069"); String sign = MogoSignature.rsaSign(signMap,private_key); System.out.println(sign); signMap.put("sign",sign); boolean r = MogoSignature.verify(signMap,sign,public_key); Assert.assertTrue(r); } }
单元测试写好后我们可以立即验证我们程序的逻辑是否正确,并且要保证每个逻辑都要覆盖到(业务逻辑的代码的测试覆盖率应该是100%)。
### 四、单元测试的好处
写单元测试确实会占用一些时间,但一旦养成习惯就离不开单元测试,因为会上瘾。一旦代码没写单元测试心里就会不安,觉得不靠谱。单元测试可以让我们立即得到反馈不用等到集成测试由测试人员提出bug。单元测试一旦写好结合maven的生命周期每次持续集成都会自动运行这些测试代码,比手动测试效率高很多,而且如果修改了代码担心影响以前逻辑,以前写好单元测试就可以自动帮我们回归测试,有了多重的保障,有了单元测试我们能对自己的代码更有安全感,更有信心,上线发布前更从容,睡好觉说不定头发也会更浓密。
希望大家能从主观先认识到单元测试的重要性和好处,这样才能真正的改善代码的质量,从而进一步改善我们的生活质量。我们希望快乐工作,开心生活,因为快乐的工作者,才能带给用户美好有价值的产品。