單元測試(Unit testing)

  有些東西嚐到甜頭才以爲它的好,單元測試(後續就簡稱ut)對我來講就是這樣。無論你在作的項目是鬆仍是緊,良好的ut都會讓你事半功倍。java

  UT的定義能夠打開https://en.wikipedia.org/wiki/Unit_testing進行一下了解,文中提到的寫UT的幾個好處確實深有體會。程序員

 寫UT能給你帶來什麼?

  • Finds problems early 更早的發現bug,而不是在你全部代碼都開發完成以後,在你提交測試以後。咱們每寫完一個功能點,完成一個接口,都要問本身一句:它有問題嗎?當你沒法確認的回答本身沒問題的時候,就應該寫一寫UT了。當你的代碼提交測試的時候本身內心都沒有一點譜,能夠說你不是一個有責任心的程序員。
  • Facilitates change 能夠理解爲讓你可以」擁抱變化「。這裏的」變化「能夠是需求的變動(這是必定會發生的,不要埋怨產品經理了),本身進行的代碼重構(沒有UT進行重構我只能問一句誰給你的勇氣)等一切會致使代碼變更的東西。代碼改變了,你如何儘量保證它仍是正確的呢,UT能夠做爲你驗證代碼的手段。不管代碼怎麼變,只要UT經過,你就能夠放心的改動代碼,笑對需求變動。

如何寫UT?

  下面就本身實踐的一些東西和你們分享下,不必定是正確的,只是我目前寫UT的方式。很歡迎你們批評指正。web

  編程語言java,測試框架junit+mockito,你們能夠換成本身使用的測試框架。maven依賴:spring

        <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接口maven

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。ide

 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服務,須要咱們本身評估;面對複雜繁多的業務場景,是否要所有測試到,是否能測試到都會是咱們面臨的問題。總之,只有每行代碼都是通過單元測試的,咱們才能說編碼工做完成了。

相關文章
相關標籤/搜索