有些東西嚐到甜頭才以爲它的好,單元測試(後續就簡稱ut)對我來講就是這樣。無論你在作的項目是鬆仍是緊,良好的ut都會讓你事半功倍。java
UT的定義能夠打開https://en.wikipedia.org/wiki/Unit_testing進行一下了解,文中提到的寫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 }
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接口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 }
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 }
下面開始編寫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服務,須要咱們本身評估;面對複雜繁多的業務場景,是否要所有測試到,是否能測試到都會是咱們面臨的問題。總之,只有每行代碼都是通過單元測試的,咱們才能說編碼工做完成了。