单元测试(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的写法。项目结构:

    单元测试(Unit testing)_第1张图片

  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服务,需要我们自己评估;面对复杂繁多的业务场景,是否要全部测试到,是否能测试到都会是我们面临的问题。总之,只有每行代码都是经过单元测试的,我们才能说编码工作完成了。

 

你可能感兴趣的:(单元测试(Unit testing))