单元测试(Unit testing)
有些东西尝到甜头才觉得它的好,单元测试(后续就简称ut)对我来说就是这样。不管你在做的项目是松还是紧,良好的ut都会让你事半功倍。
UT的定义可以打开https://en.wikipedia.org/wiki/Unit_testing进行一下了解,文中提到的写UT的几个好处确实深有体会。
写UT能给你带来什么?
- Finds problems early 更早的发现bug,而不是在你所有代码都开发完成之后,在你提交测试之后。我们每写完一个功能点,完成一个接口,都要问自己一句:它有问题吗?当你无法确认的回答自己没问题的时候,就应该写一写UT了。当你的代码提交测试的时候自己心里都没有一点谱,可以说你不是一个有责任心的程序员。
- Facilitates change 可以理解为让你能够”拥抱变化“。这里的”变化“可以是需求的变更(这是一定会发生的,不要埋怨产品经理了),自己进行的代码重构(没有UT进行重构我只能问一句谁给你的勇气)等一切会导致代码变动的东西。代码改变了,你如何尽可能保证它还是正确的呢,UT可以作为你验证代码的手段。无论代码怎么变,只要UT通过,你就可以放心的改动代码,笑对需求变更。
如何写UT?
下面就自己实践的一些东西和大家分享下,不一定是正确的,只是我目前写UT的方式。很欢迎大家批评指正。
编程语言java,测试框架junit+mockito,大家可以换成自己使用的测试框架。maven依赖:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>1.10.19</version> </dependency>
以一个简单的查询小米手机的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 @Override 43 public String toString() { 44 return "MiOneDto [id=" + id + ", type=" + type + ", salePrice=" + salePrice + ", stockQty=" + stockQty + "]"; 45 } 46 47 }
MiOneDto
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 }
MiOneDao
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 }
MiOneSalePriceService
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 @Override 17 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和setter 34 35 36 37 }
MiOneServiceImpl
下面开始编写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 @Before 25 public void before(){ 26 MockitoAnnotations.initMocks(this); 27 } 28 29 @Spy 30 MiOneServiceImpl miOneService; 31 32 @Mock 33 MiOneDao miOneDao; 34 35 @Mock 36 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 @Test 46 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手机,库存是10 55 when(miOneDao.queryUniqueMiOne(eq(miOneId))).thenReturn(miOneDto); 56 //当使用 001 id查询价格的时候返回1999 57 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的用法大家可以自行参考官方文档http://mockito.org/ 或者使用自己的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 @Override 20 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和setter 39 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<MiOneDto> (){ 100 101 @Override 102 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 @Test 133 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 查询价格的时候返回1999 142 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 查询价格的时候返回1600 151 when(salePriceService.querySalePrice(eq("002"))).thenReturn(new BigDecimal("1600")); 152 153 //销售金额>=1999时,返回优惠金额100 154 when(favourablePriceService.queryFavourablePrice(argThat(new org.mockito.ArgumentMatcher<MiOneDto> (){ 155 156 @Override 157 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服务,需要我们自己评估;面对复杂繁多的业务场景,是否要全部测试到,是否能测试到都会是我们面临的问题。总之,只有每行代码都是经过单元测试的,我们才能说编码工作完成了。