有些东西尝到甜头才觉得它的好,单元测试(后续就简称ut)对我来说就是这样。不管你在做的项目是松还是紧,良好的ut都会让你事半功倍。
UT的定义可以打开进行一下了解,文中提到的写UT的几个好处确实深有体会。
写UT能给你带来什么?
- Finds problems early 更早的发现bug,而不是在你所有代码都开发完成之后,在你提交测试之后。我们每写完一个功能点,完成一个接口,都要问自己一句:它有问题吗?当你无法确认的回答自己没问题的时候,就应该写一写UT了。当你的代码提交测试的时候自己心里都没有一点谱,可以说你不是一个有责任心的程序员。
- Facilitates change 可以理解为让你能够”拥抱变化“。这里的”变化“可以是需求的变更(这是一定会发生的,不要埋怨产品经理了),自己进行的代码重构(没有UT进行重构我只能问一句谁给你的勇气)等一切会导致代码变动的东西。代码改变了,你如何尽可能保证它还是正确的呢,UT可以作为你验证代码的手段。无论代码怎么变,只要UT通过,你就可以放心的改动代码,笑对需求变更。
如何写UT?
下面就自己实践的一些东西和大家分享下,不一定是正确的,只是我目前写UT的方式。很欢迎大家批评指正。
编程语言java,测试框架junit+mockito,大家可以换成自己使用的测试框架。maven依赖:
junit junit 4.11 test org.mockito mockito-core 1.10.19
以一个简单的查询小米手机的service为例,来说明UT的写法。项目结构:
MiOneDto:小米手机实体类
1 package com.itany.ut.dto; 2 3 import java.math.BigDecimal; 4 5 /** 6 * 小米手机 7 */ 8 public class MiOneDto { 9 //唯一标识10 private String id;11 //型号12 private String type;13 //售价14 private BigDecimal salePrice;15 //库存16 private int stockQty;17 18 public String getId() {19 return id;20 }21 public void setId(String id) {22 this.id = id;23 }24 public String getType() {25 return type;26 }27 public void setType(String type) {28 this.type = type;29 }30 public BigDecimal getSalePrice() {31 return salePrice;32 }33 public void setSalePrice(BigDecimal salePrice) {34 this.salePrice = salePrice;35 }36 public int getStockQty() {37 return stockQty;38 }39 public void setStockQty(int stockQty) {40 this.stockQty = stockQty;41 }42 @Override43 public String toString() {44 return "MiOneDto [id=" + id + ", type=" + type + ", salePrice=" + salePrice + ", stockQty=" + stockQty + "]";45 }46 47 }
MiOneDao:查询数据库接口
1 package com.itany.ut.dao;2 3 import com.itany.ut.dto.MiOneDto;4 5 public interface MiOneDao {6 7 public MiOneDto queryUniqueMiOne(String id);8 }
MiOneSalePriceService:查询价格的webservice接口
1 package com.itany.ut.remoteService;2 3 import java.math.BigDecimal;4 5 public interface MiOneSalePriceService {6 7 public BigDecimal querySalePrice(String miOneId);8 }
MiOneServiceImpl:小米手机查询service实现类
1 package com.itany.ut.service.impl; 2 3 import java.math.BigDecimal; 4 5 import com.itany.ut.dao.MiOneDao; 6 import com.itany.ut.dto.MiOneDto; 7 import com.itany.ut.remoteService.MiOneSalePriceService; 8 import com.itany.ut.service.MiOneService; 9 10 public class MiOneServiceImpl implements MiOneService{11 12 private MiOneDao miOneDao;13 14 private MiOneSalePriceService salePriceService;15 16 @Override17 public MiOneDto queryUniqueMiOne(String id) {18 MiOneDto miOneDto = miOneDao.queryUniqueMiOne(id);19 if(miOneDto != null){20 BigDecimal salePrice = salePriceService.querySalePrice(id);21 miOneDto.setSalePrice(checkPrice(salePrice));22 }23 return miOneDto;24 }25 26 private BigDecimal checkPrice(BigDecimal price){27 if(price == null || price.compareTo(BigDecimal.ZERO) < 0){28 return BigDecimal.ZERO;29 }30 return price;31 }32 33 //省略getter和setter34 35 36 37 }
下面开始编写MiOneService的的UT类MiOneServiceTest。
1 package com.itany.ut.service; 2 import static org.mockito.Matchers.*; 3 import static org.mockito.Mockito.*; 4 import static org.junit.Assert.*; 5 6 import java.math.BigDecimal; 7 8 import org.junit.Before; 9 import org.junit.Test;10 import org.mockito.Mock;11 import org.mockito.MockitoAnnotations;12 import org.mockito.Spy;13 14 import com.itany.ut.dao.MiOneDao;15 import com.itany.ut.dto.MiOneDto;16 import com.itany.ut.remoteService.MiOneSalePriceService;17 import com.itany.ut.service.impl.MiOneServiceImpl;18 19 /**20 * 查询小米手机单元测试21 */22 public class MiOneServiceTest {23 24 @Before25 public void before(){26 MockitoAnnotations.initMocks(this);27 }28 29 @Spy30 MiOneServiceImpl miOneService;31 32 @Mock33 MiOneDao miOneDao;34 35 @Mock36 MiOneSalePriceService salePriceService;37 38 public void init(){39 //使用spring @Autowired 的可以使用spring-test的工具类ReflectionTestUtils.setField进行注入40 //如果你的service用到了静态类的一些方法,是直接使用XX.xx()调用的,可以考虑在service中申明一个该类的实例,方便进行单元测试41 miOneService.setMiOneDao(miOneDao);42 miOneService.setSalePriceService(salePriceService);43 }44 45 @Test46 public void testQueryMiOne(){47 init();48 String miOneId = "001";49 50 MiOneDto miOneDto = new MiOneDto();51 miOneDto.setId("001");52 miOneDto.setType("小米3");53 miOneDto.setStockQty(10);54 //当使用 001 id 查询数据库的时候,返回一部小米3手机,库存是1055 when(miOneDao.queryUniqueMiOne(eq(miOneId))).thenReturn(miOneDto);56 //当使用 001 id查询价格的时候返回199957 when(salePriceService.querySalePrice(eq(miOneId))).thenReturn(new BigDecimal("1999"));58 //根据 001查询小米手机信息59 MiOneDto dto = miOneService.queryUniqueMiOne(miOneId);60 assertNotNull(dto);61 assertEquals(10, dto.getStockQty());62 assertEquals(miOneId,dto.getId());63 assertEquals("小米3",dto.getType());64 assertEquals(new BigDecimal("1999"),dto.getSalePrice());65 66 }67 68 }
关于Mockio的用法大家可以自行参考官方文档 或者使用自己的UT框架实现。
我们测试的是MiOneServiceImpl的queryUniqueMiOne(String id)方法,对于MiOneServiceImpl依赖的接口我们可以直接mock。单元测试一个很重要的一点是测试环境的封闭性,我不需要真正用dao查询数据库,真正的调用remoteService的接口来获取数据。反过来说,即使MiOneDao和MiOneSalePriceService还没有开发完成,我依然能够对MiOneServiceImpl进行单元测试。集成测试(integration)才需要测试不同系统、接口之间的交互。
通过testQueryMiOne这个UT我们可以测试MiOneServiceImpl调用MiOneDao和MiOneSalePriceService的时候参数传递是正确的,返回值处理的是正确的。
可能过段时间产品经理跑过来说:芃朋,我们准备举行一场优惠活动,不同型号手机有不同优惠。面对需求变更,我们需要更改现有代码,同时要增加或修改UT。
现在新增了一个webservice接口,查询优惠金额接口MiOneFavourablePriceService,代码如下:
1 package com.itany.ut.remoteService; 2 3 import java.math.BigDecimal; 4 5 import com.itany.ut.dto.MiOneDto; 6 7 public interface MioneFavourablePriceService { 8 9 /**10 * 根据类型和售价获取优惠金额11 * 小米3,售价>=1999时,优惠200元,否则优惠0元12 * 小米4,售价>=1999是,优惠100元,否则优惠0元13 */14 public BigDecimal queryFavourablePrice(MiOneDto miOneDto);15 16 }
MiOneServiceImpl类改动如下,增加了处理优惠金额的逻辑:
1 package com.itany.ut.service.impl; 2 3 import java.math.BigDecimal; 4 5 import com.itany.ut.dao.MiOneDao; 6 import com.itany.ut.dto.MiOneDto; 7 import com.itany.ut.remoteService.MiOneSalePriceService; 8 import com.itany.ut.remoteService.MioneFavourablePriceService; 9 import com.itany.ut.service.MiOneService;10 11 public class MiOneServiceImpl implements MiOneService{12 13 private MiOneDao miOneDao;14 15 private MiOneSalePriceService salePriceService;16 17 private MioneFavourablePriceService favourablePriceService;18 19 @Override20 public MiOneDto queryUniqueMiOne(String id) {21 MiOneDto miOneDto = miOneDao.queryUniqueMiOne(id);22 if(miOneDto != null){23 BigDecimal salePrice = salePriceService.querySalePrice(id);24 miOneDto.setSalePrice(checkPrice(salePrice));25 BigDecimal favourablePrice = favourablePriceService.queryFavourablePrice(miOneDto);26 miOneDto.setSalePrice(miOneDto.getSalePrice().subtract(checkPrice(favourablePrice)));27 }28 return miOneDto;29 }30 31 private BigDecimal checkPrice(BigDecimal price){32 if(price == null || price.compareTo(BigDecimal.ZERO) < 0){33 return BigDecimal.ZERO;34 }35 return price;36 }37 38 //省略getter和setter39 40 41 }
我们在获取到销售价格的基础上,再调用MioneFavourablePriceService获取商品优惠金额,然后用销售价格减去优惠金额作为手机真正的销售金额。下面我们来看一下UT:
testQueryMiOne方法应该还是测试通过的,需要增加优惠金额的测试方法。
1 package com.itany.ut.service; 2 import static org.mockito.Matchers.*; 3 import static org.mockito.Mockito.*; 4 import static org.junit.Assert.*; 5 6 import java.math.BigDecimal; 7 8 import org.junit.Before; 9 import org.junit.Test; 10 import org.mockito.Mock; 11 import org.mockito.MockitoAnnotations; 12 import org.mockito.Spy; 13 14 import com.itany.ut.dao.MiOneDao; 15 import com.itany.ut.dto.MiOneDto; 16 import com.itany.ut.remoteService.MiOneSalePriceService; 17 import com.itany.ut.remoteService.MioneFavourablePriceService; 18 import com.itany.ut.service.impl.MiOneServiceImpl; 19 20 /** 21 * 查询小米手机单元测试 22 */ 23 public class MiOneServiceTest { 24 25 @Before 26 public void before(){ 27 MockitoAnnotations.initMocks(this); 28 } 29 30 @Spy 31 MiOneServiceImpl miOneService; 32 33 @Mock 34 MiOneDao miOneDao; 35 36 @Mock 37 MiOneSalePriceService salePriceService; 38 39 @Mock 40 MioneFavourablePriceService favourablePriceService; 41 42 public void init(){ 43 //使用spring @Autowired 的可以使用spring-test的工具类ReflectionTestUtils.setField进行注入 44 //如果你的service用到了静态类的一些方法,是直接使用XX.xx()调用的,可以考虑在service中申明一个该类的实例,方便进行单元测试 45 miOneService.setMiOneDao(miOneDao); 46 miOneService.setSalePriceService(salePriceService); 47 miOneService.setFavourablePriceService(favourablePriceService); 48 } 49 /** 50 * 无优惠 51 */ 52 @Test 53 public void testQueryMiOne(){ 54 init(); 55 String miOneId = "001"; 56 57 MiOneDto miOneDto = new MiOneDto(); 58 miOneDto.setId("001"); 59 miOneDto.setType("小米3"); 60 miOneDto.setStockQty(10); 61 //当使用 001 id 查询数据库的时候,返回一部小米3手机,库存是10 62 when(miOneDao.queryUniqueMiOne(eq(miOneId))).thenReturn(miOneDto); 63 //当使用 001 id查询价格的时候返回1999 64 when(salePriceService.querySalePrice(eq(miOneId))).thenReturn(new BigDecimal("1999")); 65 //根据 001查询小米手机信息 66 MiOneDto dto = miOneService.queryUniqueMiOne(miOneId); 67 assertNotNull(dto); 68 assertEquals(10, dto.getStockQty()); 69 assertEquals(miOneId,dto.getId()); 70 assertEquals("小米3",dto.getType()); 71 assertEquals(new BigDecimal("1999"),dto.getSalePrice()); 72 73 } 74 /** 75 * 小米3手机优惠测试 76 */ 77 @Test 78 public void testMiOne3FavourablePrice(){ 79 init(); 80 MiOneDto miOneDto1 = new MiOneDto(); 81 miOneDto1.setId("001"); 82 miOneDto1.setType("小米3"); 83 miOneDto1.setStockQty(10); 84 //当使用 001 id 查询数据库的时候,返回一部小米3手机 85 when(miOneDao.queryUniqueMiOne(eq("001"))).thenReturn(miOneDto1); 86 //当使用 001 id 查询价格的时候返回1999 87 when(salePriceService.querySalePrice(eq("001"))).thenReturn(new BigDecimal("1999")); 88 89 MiOneDto miOneDto2 = new MiOneDto(); 90 miOneDto2.setId("002"); 91 miOneDto2.setType("小米3"); 92 miOneDto2.setStockQty(10); 93 //当使用 002 id 查询数据库的时候,返回一部小米3手机 94 when(miOneDao.queryUniqueMiOne(eq("002"))).thenReturn(miOneDto2); 95 //当使用 002 id 查询价格的时候返回1600 96 when(salePriceService.querySalePrice(eq("002"))).thenReturn(new BigDecimal("1600")); 97 98 //销售金额>=1999时,返回优惠金额200 99 when(favourablePriceService.queryFavourablePrice(argThat(new org.mockito.ArgumentMatcher(){100 101 @Override102 public boolean matches(Object argument) {103 MiOneDto dto = (MiOneDto)argument;104 if(dto != null && "小米3".equals(dto.getType()) && dto.getSalePrice().compareTo(new BigDecimal("1999")) >= 0){105 return true;106 }107 return false;108 }109 110 }))).thenReturn(new BigDecimal("200"));111 112 //根据 001查询小米手机信息113 MiOneDto dto1 = miOneService.queryUniqueMiOne("001");114 assertNotNull(dto1);115 assertEquals(10, dto1.getStockQty());116 assertEquals("001",dto1.getId());117 assertEquals("小米3",dto1.getType());118 assertEquals(new BigDecimal("1799"),dto1.getSalePrice());119 120 //根据 002查询小米手机信息121 MiOneDto dto2 = miOneService.queryUniqueMiOne("002");122 assertNotNull(dto2);123 assertEquals(10, dto2.getStockQty());124 assertEquals("002",dto2.getId());125 assertEquals("小米3",dto2.getType());126 assertEquals(new BigDecimal("1600"),dto2.getSalePrice());127 }128 129 /**130 * 小米4手机优惠测试131 */132 @Test133 public void testMiOne4FavourablePrice(){134 init();135 MiOneDto miOneDto1 = new MiOneDto();136 miOneDto1.setId("001");137 miOneDto1.setType("小米4");138 miOneDto1.setStockQty(10);139 //当使用 001 id 查询数据库的时候,返回一部小米4手机140 when(miOneDao.queryUniqueMiOne(eq("001"))).thenReturn(miOneDto1);141 //当使用 001 id 查询价格的时候返回1999142 when(salePriceService.querySalePrice(eq("001"))).thenReturn(new BigDecimal("1999"));143 144 MiOneDto miOneDto2 = new MiOneDto();145 miOneDto2.setId("002");146 miOneDto2.setType("小米4");147 miOneDto2.setStockQty(10);148 //当使用 002 id 查询数据库的时候,返回一部小米4手机149 when(miOneDao.queryUniqueMiOne(eq("002"))).thenReturn(miOneDto2);150 //当使用 002 id 查询价格的时候返回1600151 when(salePriceService.querySalePrice(eq("002"))).thenReturn(new BigDecimal("1600"));152 153 //销售金额>=1999时,返回优惠金额100154 when(favourablePriceService.queryFavourablePrice(argThat(new org.mockito.ArgumentMatcher (){155 156 @Override157 public boolean matches(Object argument) {158 MiOneDto dto = (MiOneDto)argument;159 if(dto != null && "小米4".equals(dto.getType()) && dto.getSalePrice().compareTo(new BigDecimal("1999")) >= 0){160 return true;161 }162 return false;163 }164 165 }))).thenReturn(new BigDecimal("100"));166 167 //根据 001查询小米手机信息168 MiOneDto dto1 = miOneService.queryUniqueMiOne("001");169 assertNotNull(dto1);170 assertEquals(10, dto1.getStockQty());171 assertEquals("001",dto1.getId());172 assertEquals("小米4",dto1.getType());173 assertEquals(new BigDecimal("1899"),dto1.getSalePrice());174 175 //根据 002查询小米手机信息176 MiOneDto dto2 = miOneService.queryUniqueMiOne("002");177 assertNotNull(dto2);178 assertEquals(10, dto2.getStockQty());179 assertEquals("002",dto2.getId());180 assertEquals("小米4",dto2.getType());181 assertEquals(new BigDecimal("1600"),dto2.getSalePrice());182 }183 184 }
通过testMiOne3FavourablePrice()和testMiOne4FavourablePrice()方法,可以验证我们新增的优惠金额功能是否正确;通过testQueryMiOne()保证修改后的代码没有对之前的业务逻辑造成影响。
上面只是通过一个简单的例子说明java中UT的写法(临界值和异常测试没有包含)。UT的颗粒度是要精细到每个方法,还是到某个service服务,需要我们自己评估;面对复杂繁多的业务场景,是否要全部测试到,是否能测试到都会是我们面临的问题。总之,只有每行代码都是经过单元测试的,我们才能说编码工作完成了。