前言
本文主要提供了一種單元測(cè)試方法,力求0基礎(chǔ)人員可以從本文中受到啟發(fā),可以搭建一套好用的單元測(cè)試環(huán)境,并能切實(shí)的提高交付代碼的質(zhì)量。極簡(jiǎn)體現(xiàn)在除了POM依賴和單元測(cè)試類之外,其他什么都不需要引入,只需要一個(gè)本地能啟動(dòng)的springboot項(xiàng)目。
目錄
1.POM依賴
2.單元測(cè)試類示例及注解釋義
3.單元測(cè)試經(jīng)驗(yàn)總結(jié)
一、POM依賴
Springboot版本: 2.6.6
org.springframework.boot/groupId?> spring-boot-starter-test/artifactId?> test/scope?> /dependency?> org.mockito/groupId?> mockito-core/artifactId?> 3.12.4/version?> /dependency?>
二、單元測(cè)試類示例
主要有兩種
第一種,偏集成測(cè)試
需要啟動(dòng)項(xiàng)目,需要連接數(shù)據(jù)庫(kù)、RPC注冊(cè)中心等
主要注解:@SpringBootTest + @RunWith(SpringRunner.class) + @Transactional + @Resource + @SpyBean + @Test
?@SpringBootTest + @RunWith(SpringRunner.class) 啟動(dòng)了一套springboot的測(cè)試環(huán)境;
?@Transactional 對(duì)于一些修改數(shù)據(jù)庫(kù)的操作,會(huì)執(zhí)行回滾,能測(cè)試執(zhí)行sql,但是又不會(huì)真正的修改測(cè)試庫(kù)的數(shù)據(jù);
?@Resource 主要引入被測(cè)試的類
?@SpyBean springboot環(huán)境下mock依賴的bean,可以搭配Mockito.doAnswer(…).when(xxServiceImpl).xxMethod(any())mock特定方法的返回值;
?@Test 標(biāo)識(shí)一個(gè)測(cè)試方法
TIP:對(duì)于打樁有這幾個(gè)注解@Mock @Spy @MockBean @SpyBean,每一個(gè)都有其對(duì)應(yīng)的搭配,簡(jiǎn)單說@Mock和@Spy要搭配@InjectMocks去使用,@MockBean和@SpyBean搭配@SpringBootTest + @RunWith(SpringRunner.class)使用,@InjectMocks不用啟動(dòng)應(yīng)用,它啟動(dòng)了一個(gè)完全隔離的測(cè)試環(huán)境,無法使用spring提供的所有bean,所有的依賴都需要被mock
上代碼:
/** * @author jiangbo8 * @since 2024/4/24 9:52 */ @Transactional @SpringBootTest @RunWith(SpringRunner.class) public class SalesAmountPlanControllerAppTest { @Resource private SalesAmountPlanController salesAmountPlanController; @SpyBean private ISaleAmountHourHistoryService saleAmountHourHistoryServiceImpl; @SpyBean private ISaleAmountHourForecastService saleAmountHourForecastServiceImpl; @SpyBean private ISaleAmountHourPlanService saleAmountHourPlanServiceImpl; @Test public void testGraph1() { // 不寫mock就走實(shí)際調(diào)用 SalesAmountDTO dto = new SalesAmountDTO(); dto.setDeptId1List(Lists.newArrayList(35)); dto.setDeptId2List(Lists.newArrayList(235)); dto.setDeptId3List(Lists.newArrayList(100)); dto.setYoyType(YoyTypeEnum.SOLAR.getCode()); dto.setShowWeek(true); dto.setStartYm("2024-01"); dto.setEndYm("2024-10"); dto.setTimeDim(GraphTimeDimensionEnum.MONTH.getCode()); dto.setDataType(SalesAmountDataTypeEnum.AMOUNT.getCode()); Result result = salesAmountPlanController.graph(dto); System.out.println(JSON.toJSONString(result)); Assert.assertNotNull(result); } @Test public void testGraph11() { // mock就走mock Mockito.doAnswer(this::mockSaleAmountHourHistoryListQuery).when(saleAmountHourHistoryServiceImpl).listBySaleAmountQueryBo(any()); Mockito.doAnswer(this::mockSaleAmountHourPlansListQuery).when(saleAmountHourPlanServiceImpl).listBySaleAmountQueryBo(any()); Mockito.doAnswer(this::mockSaleAmountHourForecastListQuery).when(saleAmountHourForecastServiceImpl).listBySaleAmountQueryBo(any()); SalesAmountDTO dto = new SalesAmountDTO(); dto.setDeptId1List(Lists.newArrayList(111)); dto.setDeptId2List(Lists.newArrayList(222)); dto.setDeptId3List(Lists.newArrayList(333)); dto.setYoyType(YoyTypeEnum.SOLAR.getCode()); dto.setShowWeek(true); dto.setStartYm("2024-01"); dto.setEndYm("2024-10"); dto.setTimeDim(GraphTimeDimensionEnum.MONTH.getCode()); dto.setDataType(SalesAmountDataTypeEnum.AMOUNT.getCode()); Result result = salesAmountPlanController.graph(dto); System.out.println(JSON.toJSONString(result)); Assert.assertNotNull(result); } private List mockSaleAmountHourHistoryListQuery(org.mockito.invocation.InvocationOnMock s) { SaleAmountQueryBo queryBo = s.getArgument(0); if (queryBo.getGroupBy().contains("ymd")) { List historyList = Lists.newArrayList(); List ymdList = DateUtil.rangeWithDay(DateUtil.parseFirstDayLocalDate(queryBo.getStartYm()), DateUtil.parseLastDayLocalDate(queryBo.getStartYm())); for (String ymd : ymdList) { SaleAmountHourHistory history = new SaleAmountHourHistory(); history.setYear(Integer.parseInt(queryBo.getStartYm().split("-")[0])); history.setMonth(Integer.parseInt(queryBo.getStartYm().split("-")[1])); history.setYm(queryBo.getStartYm()); history.setYmd(DateUtil.parseLocalDateByYmd(ymd)); history.setAmount(new BigDecimal("1000")); history.setAmountSp(new BigDecimal("2000")); history.setAmountLunarSp(new BigDecimal("3000")); history.setSales(new BigDecimal("100")); history.setSalesSp(new BigDecimal("200")); history.setSalesLunarSp(new BigDecimal("300")); history.setCostPrice(new BigDecimal("100")); history.setCostPriceSp(new BigDecimal("100")); history.setCostPriceLunarSp(new BigDecimal("100")); historyList.add(history); } return historyList; } List ymList = DateUtil.rangeWithMonth(DateUtil.parseFirstDayLocalDate(queryBo.getStartYm()), DateUtil.parseLastDayLocalDate(queryBo.getEndYm())); List historyList = Lists.newArrayList(); for (String ym : ymList) { SaleAmountHourHistory history = new SaleAmountHourHistory(); history.setYear(Integer.parseInt(ym.split("-")[0])); history.setMonth(Integer.parseInt(ym.split("-")[1])); history.setYm(ym); history.setAmount(new BigDecimal("10000")); history.setAmountSp(new BigDecimal("20000")); history.setAmountLunarSp(new BigDecimal("30000")); history.setSales(new BigDecimal("1000")); history.setSalesSp(new BigDecimal("2000")); history.setSalesLunarSp(new BigDecimal("3000")); history.setCostPrice(new BigDecimal("100")); history.setCostPriceSp(new BigDecimal("100")); history.setCostPriceLunarSp(new BigDecimal("100")); historyList.add(history); } return historyList; } }
第二種,單元測(cè)試
不需要啟動(dòng)項(xiàng)目,也不會(huì)連接數(shù)據(jù)庫(kù)、RPC注冊(cè)中心等,但是相應(yīng)的所有數(shù)據(jù)都需要打樁mock
這種方法可以使用testMe快速生成單元測(cè)試類的框架,具體方法見: 基于testMe快速生成單元測(cè)試類(框架)
主要注解:@InjectMocks + @Mock + @Test
?@InjectMocks標(biāo)識(shí)了一個(gè)需要被測(cè)試的類,這個(gè)類中依賴的bean都需要被@Mock,并mock返回值,不然就會(huì)空指針
?@Mock mock依賴,具體mock數(shù)據(jù)還要搭配when(xxService.xxMethod(any())).thenReturn(new Object()); mock返回值
?@Test 標(biāo)識(shí)一個(gè)測(cè)試方法
上代碼:
/** * Created by jiangbo8 on 2022/10/17 15:02 */ public class CheckAndFillProcessorTest { @Mock Logger log; @Mock OrderRelService orderRelService; @Mock VenderServiceSdk venderServiceSdk; @Mock AfsServiceSdk afsServiceSdk; @Mock PriceServiceSdk priceServiceSdk; @Mock ProductInfoSdk productInfoSdk; @Mock OrderMidServiceSdk orderMidServiceSdk; @Mock OrderQueueService orderQueueService; @Mock SendpayMarkService sendpayMarkService; @Mock TradeOrderService tradeOrderService; @InjectMocks CheckAndFillProcessor checkAndFillProcessor; @Before public void setUp() { MockitoAnnotations.initMocks(this); } @Test public void testProcess2() throws Exception { OrderRel orderRel = new OrderRel(); //orderRel.setJdOrderId(2222222L); orderRel.setSopOrderId(1111111L); orderRel.setVenderId("123"); when(orderRelService.queryOrderBySopOrderId(anyLong())).thenReturn(orderRel); OrderDetailRel orderDetailRel = new OrderDetailRel(); orderDetailRel.setJdSkuId(1L); when(orderRelService.queryDetailList(any())).thenReturn(Collections.singletonList(orderDetailRel)); Vender vender = new Vender(); vender.setVenderId("123"); vender.setOrgId(1); when(venderServiceSdk.queryVenderByVenderId(anyString())).thenReturn(vender); when(afsServiceSdk.queryAfsTypeByJdSkuAndVender(anyLong(), anyString())).thenReturn(0); when(priceServiceSdk.getJdToVenderPriceByPriorityAndSaleTime(anyString(), anyString(), any())).thenReturn(new BigDecimal("1")); when(productInfoSdk.getProductInfo(any())).thenReturn(new HashMap>() {{ put(1L, new HashMap() {{ put("String", "String"); }}); }}); when(orderQueueService.updateQueueBySopOrderId(any())).thenReturn(true); Order sopOrder = new Order(); sopOrder.setYn(1); when(orderMidServiceSdk.getOrderByIdFromMiddleWare(anyLong())).thenReturn(sopOrder); when(sendpayMarkService.isFreshOrder(anyLong(), anyString())).thenReturn(true); doNothing().when(tradeOrderService).fillOrderProduceTypeInfo(any(), anyInt(), any()); doNothing().when(tradeOrderService).fillOrderFlowFlagInfo(any(), any(), anyInt(), any()); Field field = ResourceContainer.class.getDeclaredField("allInPlateConfig"); field.setAccessible(true); field.set("allInPlateConfig", new AllInPlateConfig()); OrderQueue orderQueue = new OrderQueue(); orderQueue.setSopOrderId(1111111L); DispatchResult result = checkAndFillProcessor.process(orderQueue); Assert.assertNotNull(result); } }
三、單元測(cè)試經(jīng)驗(yàn)總結(jié)
在工作中總結(jié)了一些單元測(cè)試的使用場(chǎng)景:
1.重構(gòu),如果我們拿到了一個(gè)代碼,我們要去重構(gòu)這個(gè)代碼,如果這個(gè)代碼本身的單元測(cè)試比較完善,那么我們重構(gòu)完之后可以執(zhí)行一下現(xiàn)有的單元測(cè)試,以保證重構(gòu)前后代碼在各個(gè)場(chǎng)景的邏輯保證最終一致,但是如果單元測(cè)試不完善甚至沒有,那我建議大家可以基于AI去生成這個(gè)代碼的單元測(cè)試,然后進(jìn)行重構(gòu),再用生成的單元測(cè)試去把控質(zhì)量,這里推薦Diffblue去生成,有興趣的可以去了解一下。
2.新功能,新功能建議使用上面推薦的兩種方法去做單測(cè),第一種方法因?yàn)槠蓽y(cè)試,單元測(cè)試代碼編寫的壓力比較小,可以以黑盒測(cè)試的視角去覆蓋測(cè)試case就可以了,但是如果某場(chǎng)景極為復(fù)雜,想要單獨(dú)對(duì)某個(gè)復(fù)雜計(jì)算代碼塊進(jìn)行專門的測(cè)試,那么可以使用第二種方法,第二種方法是很單純的單元測(cè)試,聚焦專門代碼塊,但是如果普遍使用的話,單元測(cè)試代碼編寫量會(huì)很大,不建議單純使用某一種,可以具體情況具體分析。
建議大家做單元測(cè)試不要單純的追求行覆蓋率,還是要本著提高質(zhì)量的心態(tài)去做單元測(cè)試。
審核編輯 黃宇
-
測(cè)試
+關(guān)注
關(guān)注
8文章
5629瀏覽量
128308 -
單元測(cè)試
+關(guān)注
關(guān)注
0文章
49瀏覽量
3278
發(fā)布評(píng)論請(qǐng)先 登錄
電源與時(shí)鐘的單元測(cè)試方案解析
MCU進(jìn)行單元測(cè)試的方法
單元測(cè)試/集成測(cè)試自動(dòng)化工具--WinAMS
如何提高嵌入式軟件單元測(cè)試效率
系統(tǒng)測(cè)試、單元測(cè)試、集成測(cè)試、驗(yàn)收測(cè)試、回歸測(cè)試
單元測(cè)試常用的方法

什么是單元測(cè)試_單元測(cè)試的目的是什么
java單元測(cè)試的好處
java單元測(cè)試怎么寫

什么是單元測(cè)試,為什么要做單元測(cè)試
MCU如何進(jìn)行單元測(cè)試

RT-Thread上的單元測(cè)試:什么是單元測(cè)試?單元測(cè)試的作用是什么?

一種通用的汽車車身電子單元測(cè)試工裝的研究設(shè)計(jì)

嵌入軟件單元測(cè)試工具的作用

評(píng)論