靈魂拷問:到底要不要寫單元測(cè)試,如何正確進(jìn)行單元測(cè)試?
點(diǎn)擊上方“碼農(nóng)突圍”,馬上關(guān)注
這里是碼農(nóng)充電第一站,回復(fù)“666”,獲取一份專屬大禮包 真愛,請(qǐng)?jiān)O(shè)置“星標(biāo)”或點(diǎn)個(gè)“在看
來源:blog.csdn.net/new_com/article/details/116098959
為什么要寫單元測(cè)試
一聊起測(cè)試用例,很多人第一反應(yīng)就是,我們公司的測(cè)試會(huì)寫測(cè)試用例的,我自己也會(huì)使用postman或者swagger之類的進(jìn)行代碼自測(cè)。那我們研發(fā)到底要不要寫單元測(cè)試用例呢?
參考阿里巴巴開發(fā)手冊(cè),第8條規(guī)則(單元測(cè)試的基本目標(biāo):語句覆蓋率達(dá)到 70%;核心模塊的語句覆蓋率和分支覆蓋率都要達(dá)到 100%),大廠的要求就是必須嘍。我個(gè)人感覺,寫單元測(cè)試用例也是很有必要的,好處很多,例如:
-
保證代碼質(zhì)量!!!無論初級(jí),中級(jí),高級(jí)攻城獅開發(fā)工程的代碼,且不說效率如何,功能是必要要保證是正確的;交付測(cè)試以后,bug銳減,聯(lián)調(diào)飛快。 -
代碼邏輯“文檔化”!!!新人接手維護(hù)模塊代碼時(shí),通過單元測(cè)試用例,以debug的方式就能熟悉業(yè)務(wù)代碼。比起,看代碼,研究表結(jié)構(gòu)梳理代碼結(jié)構(gòu),效率提升飛快。 -
易維護(hù)!!!新人接手維護(hù)代碼模塊時(shí),提交自己的代碼時(shí),遠(yuǎn)行之前的單元測(cè)試達(dá)到回歸測(cè)試,保證了新改動(dòng)不會(huì)影響老業(yè)務(wù)。 -
快速定位bug!!!在聯(lián)調(diào)期間,測(cè)試提出bug后,基于uat環(huán)境,編寫出錯(cuò)的api測(cè)試用例。根據(jù),測(cè)試提供的參數(shù)和token就可以以debug的方式跟蹤問題的所在,如果是在微服務(wù)架構(gòu)中,運(yùn)行單元測(cè)試用例,不會(huì)注冊(cè)本地服務(wù)到uat環(huán)境,還能過正常請(qǐng)求注冊(cè)中心的服務(wù)。
到底如何寫單元測(cè)試
Java開發(fā)springboot項(xiàng)目都是基于junit測(cè)試框架,比較MockitoJUnitRunner與SpringRunner的使用,MockitoJUnitRunner基于mockito,模擬業(yè)務(wù)條件,驗(yàn)證代碼邏輯。SpringRunner是MockitoJUnitRunner子類,集成了Spring容器,可以在測(cè)試的根據(jù)配置加載Spring bean對(duì)象。
在Springboot開發(fā)中,結(jié)合@SpringBootTest注解,加載項(xiàng)目配置,進(jìn)行單元測(cè)試。
基于MockitoJUnitRunner的方法測(cè)試
以springboot項(xiàng)目為例,一般,對(duì)單個(gè)的方法都是進(jìn)行mock測(cè)試,在測(cè)試方法使用MockitoJUnitRunner,根據(jù)不同條件覆蓋測(cè)試。使用@InjectMocks注解,可以讓模擬的方法正常發(fā)起請(qǐng)求;@Mock注解可以模擬期望的條件。以刪除菜單服務(wù)為例,源碼如下:
@Service
public class MenuManagerImpl implements IMenuManager {
/**
* 刪除菜單業(yè)務(wù)邏輯
**/
@Override
@OptimisticRetry
@Transactional(rollbackFor = Exception.class)
public boolean delete(Long id) {
if (Objects.isNull(id)) {
return false;
}
Menu existingMenu = this.menuService.getById(id);
if (Objects.isNull(existingMenu)) {
return false;
}
if (!this.menuService.removeById(id)) {
throw new OptimisticLockingFailureException("刪除菜單失敗!");
}
return true;
}
}
/**
* 刪除菜單方法級(jí)單元測(cè)試用例
**/
@RunWith(MockitoJUnitRunner.class)
public class MenuManagerImplTest {
@InjectMocks
private MenuManagerImpl menuManager;
@Mock
private IMenuService menuService;
@Test
public void delete() {
Long id = null;
boolean flag;
// id為空
flag = menuManager.delete(id);
Assert.assertFalse(flag);
// 菜單返回為空
id = 1l;
Mockito.when(this.menuService.getById(ArgumentMatchers.anyLong())).thenReturn(null);
flag = menuManager.delete(id);
Assert.assertFalse(flag);
// 修改成功
Menu mockMenu = new Menu();
Mockito.when(this.menuService.getById(ArgumentMatchers.anyLong())).thenReturn(mockMenu);
Mockito.when(this.menuService.removeById(ArgumentMatchers.anyLong())).thenReturn(true);
flag = menuManager.delete(id);
Assert.assertTrue(flag);
}
}
基于SpringRunner的Spring容器測(cè)試
在api開發(fā)過程中,會(huì)對(duì)單個(gè)api的調(diào)用鏈路進(jìn)行驗(yàn)證,對(duì)第三方服務(wù)進(jìn)行mock模擬,本服務(wù)的業(yè)務(wù)邏輯進(jìn)行測(cè)試。
一般,會(huì)使用@SpringBootTest加載測(cè)試環(huán)境的Spring容器配置,使用MockMvc以http請(qǐng)求的方式進(jìn)行測(cè)試。以修改新增菜單測(cè)試用例為例,如下:
/**
* 成功新增菜單api
*/
@Api(tags = "管理員菜單api")
@RestController
public class AdminMenuController {
@Autowired
private IMenuManager menuManager;
@PreAuthorize("hasAnyAuthority('menu:add','admin')")
@ApiOperation(value = "新增菜單")
@PostMapping("/admin/menu/add")
@VerifyLoginUser(type = IS_ADMIN, errorMsg = INVALID_ADMIN_TYPE)
public Response<MenuVo> save(@Validated @RequestBody SaveMenuDto saveMenuDto) {
return Response.success(menuManager.save(saveMenuDto));
}
}
/**
* 成功新增菜單單元測(cè)試用例
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MallSystemApplication.class)
@Slf4j
@AutoConfigureMockMvc
public class AdminMenuControllerTest extends BaseTest {
/**
* 成功新增菜單
*/
@Test
public void success2save() throws Exception {
SaveMenuDto saveMenuDto = new SaveMenuDto();
saveMenuDto.setName("重置密碼");
saveMenuDto.setParentId(1355339254819966978l);
saveMenuDto.setOrderNum(4);
saveMenuDto.setType(MenuType.button.getValue());
saveMenuDto.setVisible(MenuVisible.show.getValue());
saveMenuDto.setUrl("https:baidu.com");
saveMenuDto.setMethod(MenuMethod.put.getValue());
saveMenuDto.setPerms("user:reset-pwd");
// 發(fā)起http請(qǐng)求
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
.post("/admin/menu/add")
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(JSON.toJSONString(saveMenuDto))
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
.header(GlobalConstant.AUTHORIZATION_HEADER, GlobalConstant.ADMIN_TOKEN))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
Response<MenuVo> response = JSON.parseObject(mvcResult.getResponse().getContentAsString(), menuVoTypeReference);
// 斷言結(jié)果
Assert.assertNotNull(response);
MenuVo menuVo;
Assert.assertNotNull(menuVo = response.getData());
Assert.assertEquals(menuVo.getName(), saveMenuDto.getName());
Assert.assertEquals(menuVo.getOrderNum(), saveMenuDto.getOrderNum());
Assert.assertEquals(menuVo.getType(), saveMenuDto.getType());
Assert.assertEquals(menuVo.getVisible(), saveMenuDto.getVisible());
Assert.assertEquals(menuVo.getStatus(), MenuStatus.normal.getValue());
Assert.assertEquals(menuVo.getUrl(), saveMenuDto.getUrl());
Assert.assertEquals(menuVo.getPerms(), saveMenuDto.getPerms());
Assert.assertEquals(menuVo.getMethod(), saveMenuDto.getMethod());
}
}
具體編寫單元測(cè)試用例規(guī)則參考測(cè)試用例的編寫。簡(jiǎn)單說,一般api的單元測(cè)試用例,編寫兩類,如下:
-
業(yè)務(wù)參數(shù)的校驗(yàn),和義務(wù)異常的校驗(yàn)。例如,名稱是否為空,電話號(hào)碼是否正確,用戶未登陸則拋出未登陸異常。 -
各類業(yè)務(wù)場(chǎng)景的真實(shí)測(cè)試用例,例如,編寫成功添加頂級(jí)菜單的測(cè)試用例,已經(jīng)編寫成功添加子級(jí)菜單的測(cè)試用例。
注意事項(xiàng)
配置覆蓋
此外,如上基于mockmvc的編寫的測(cè)試用例,由于加載了Spring的配置,會(huì)對(duì)項(xiàng)目發(fā)起真實(shí)的調(diào)用。如果,環(huán)境的配置為線上配置,容易出現(xiàn)安全問題。
一般出于安全考慮,很多公司會(huì)對(duì)真實(shí)環(huán)境的修改操作做事務(wù)回滾操作,甚至根本就不會(huì)進(jìn)行真實(shí)環(huán)境的調(diào)用,使用模擬環(huán)境替換,例如數(shù)據(jù)庫(kù)的操作可以使用h2內(nèi)存數(shù)據(jù)庫(kù)進(jìn)行替換。
這時(shí),可以在src/test/resources目錄下,添加與src/main/resources目錄下,相同的文件進(jìn)行配置覆蓋。src/test/main目錄下的代碼,會(huì)首先加載src/test/resources目錄下的配置,如果沒有則在加載src/main/resources目錄的配置。常用場(chǎng)景如下:
-
在單元測(cè)試環(huán)境使用使用內(nèi)存數(shù)據(jù)庫(kù)。 -
ginkens代碼集成運(yùn)行測(cè)試用例時(shí),不希望在集成環(huán)境中輸出日志文件信息,并且以debug級(jí)別輸出日志。
以日志文件配置覆蓋為例,在src/main/resources目錄下配置日志有文件和控制臺(tái)輸出,如圖:
main/resource目錄下的logback-spring.xml,內(nèi)容如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">
<contextName>mall-system</contextName>
<!-- 控制臺(tái)日志輸出配置 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n
</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!-- 日志文件輸出配置 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>log/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>log/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>50</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level] [%contextName] [%logger{80}:%L] %msg%n</pattern>
</encoder>
</appender>
<!-- 設(shè)置INFO 級(jí)別輸出日志 -->
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
src/test//resource目錄下的新增logback-spring.xml,去掉日志文件輸出的配置,設(shè)置日志輸出級(jí)別為DEBUG;如果運(yùn)行測(cè)試用例,則加載該配置不會(huì)進(jìn)行日志文件的輸出,并且打印DEBUG級(jí)別日志。如圖:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">
<contextName>mall-system</contextName>
<!-- 控制臺(tái)日志輸出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n
</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!-- DEBUG級(jí)別日志輸出 -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
指定環(huán)境
一般開發(fā)過程中,我們研發(fā)只會(huì)操作開發(fā)環(huán)境,也是為了避免數(shù)據(jù)安全問題,可以在單元測(cè)試用例中指定運(yùn)行的環(huán)境配置。在測(cè)試類加上@ActiveProfiles("dev"),指定獲取dev環(huán)境的配置。示例,
/**
* 獲取dev環(huán)境配置
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MallSystemApplication.class)
@Slf4j
@AutoConfigureMockMvc
@ActiveProfiles("dev")
public class AdminMenuControllerTest extends BaseTest {
}
在聯(lián)調(diào)測(cè)試中,對(duì)于出錯(cuò)的api,可以編寫對(duì)應(yīng)的單元測(cè)試用例,使用@ActiveProfiles("uat")指定到測(cè)試環(huán)境,就可以根據(jù)測(cè)試提供的參數(shù)快速定位問題。示例:
/**
* 新增菜單api聯(lián)調(diào)
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MallSystemApplication.class)
@Slf4j
@AutoConfigureMockMvc
@ActiveProfiles("uat")
public class AdminMenuControllerTest extends BaseTest {
/**
* 成功新增菜單
*/
@Test
public void success2save() throws Exception {
String token="Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjhjMjhlZWEzLTA5MWEtNDA1OS1iMzliLTRjOGMyNGY4ZjEzMiJ9.xK9srWjeGaq4NXt4BzG2MQ_yN9IaYtPVjKj5MoSS4bX9Ytf1XJNe_NSupR0IItkB48G6mXVZwj5CIwWIYzvsEA";
String paramJson="{
"name":"mayuan",
"parentId":"1",
"orderNum":"1",
"type":"1",
"visible":true,
"url":"https:baidu.com",
"method":2,
"perms":"user:reset-pwd"
}";
// 發(fā)起http請(qǐng)求
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
.post("/admin/menu/add")
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(paramJson)
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
.header(GlobalConstant.AUTHORIZATION_HEADER, token))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn();
}
}
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!
點(diǎn)擊??卡片,關(guān)注后回復(fù)【 面試題】即可獲取


