前言
作為程序員為了提前發(fā)現(xiàn)代碼bug,優(yōu)化代碼; 通常我們寫完某個功能模塊代碼后都需要寫單元測試對代碼塊進(jìn)行測試(特別是敏捷開發(fā)中);Java項(xiàng)目最常用的單元測試框架即為Junit(目前最新版本為Junit5),SpringBoot本身也整合了該框架。在寫單元測試時代碼塊中的調(diào)到第三方接口方法或涉及數(shù)據(jù)庫操作的接口方法一般都需要mock掉(測試中叫打測試樁)。目前在 Java 中主流的 Mock 測試框架有 Mockito、JMock、EasyMock,Mockito 框架是SpringBoot 目前內(nèi)建的 框架。本文主要介紹Junit5+Mockito在SpringBoot項(xiàng)目寫單元測試的使用。
maven依賴
Mockito,Junit在SpringBoot 內(nèi)部已依賴只需引入spring-boot-starter-test即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
Junit5基本使用
基本注解:
類注解:
@TestInstance(Lifecycle.PER_CLASS)注解
如果您希望JUnit Jupiter在同一個測試實(shí)例上執(zhí)行所有測試方法,只需使用@TestInstance(Lifecycle.PER_CLASS)注釋您的測試類。使用此模式時,每個測試類將創(chuàng)建一個新的測試實(shí)例。如果沒使用@TestInstance(Lifecycle.PER_CLASS)注解,使用@BeforeAll和@AfterAll注解必須在static靜態(tài)方法上使用。
- TestInstance.Lifecycle.PER_CLASS:每個測試類將創(chuàng)建一個新的測試實(shí)例。
- TestInstance.Lifecycle.PER_METHOD:將為每種測試方法,測試工廠方法或測試模板方法創(chuàng)建一個新的測試實(shí)例。此模式類似于JUnit版本1至4中的行為。
@ExtendWith(MockitoExtension.class)注解
用在springboot項(xiàng)目中,涉及spring的單元測試需要使用@ExtendWith(SpringExtension.class)注解,可以mock spring bean。不涉及spring時使用@ExtendWith(MockitoExtension.class)。
@ExtendWith(SpringExtension.class)注解在Spring boot?2.1.x需要配合@SpringBootTest 使用,Spring boot?2.1.x之后可以不使用@ExtendWith(SpringExtension.class)注解
參考文檔:Java – 理解 @ExtendWith(SpringExtension.class) 和 @ExtendWith(MockitoExtension.class)之間的差別
@SpringBootTest(classes = Application.class)注解
classes = ApplicationStarter.class指向SpringBoot啟動類,啟動spring容器。
在不同的Spring Boot版本中@ExtendWith的使用:
其中在Spring boot?2.1.x之前:?
@SpringBootTest?需要配合@ExtendWith(SpringExtension.class)才能正常工作的。
而在Spring boot?2.1.x之后:?
@SpringBootTest 已經(jīng)組合了@ExtendWith(SpringExtension.class),因此,無需在進(jìn)行該注解的使用了,進(jìn)一步簡化。如下圖@SpringBootTest注解中已包含@ExtendWith(SpringExtension.class):
?
方法注解:
基本的注解都是方法上的注解,意思就是只在測試方法上進(jìn)行添加,對應(yīng)注解有以下幾種:
注解 | 說明 |
@Test | 測試方法的入口;可單獨(dú)運(yùn)行 |
@BeforeEach | 每個測試方法前運(yùn)行;不可以單獨(dú)運(yùn)行該方法 |
@AfterEach | 每個測試方法后運(yùn)行;不可以單獨(dú)運(yùn)行該方法 |
@BeforeAll? | 在類中所有方法前運(yùn)行;static修飾;不可單獨(dú)運(yùn)行該方法 |
@AfterAll | 在類中所有方法后運(yùn)行;static修飾;不可單獨(dú)運(yùn)行該方法 |
代碼示例:
import org.mockito.InjectMocks;
@TestInstance(Lifecycle.PER_CLASS)
@SpringBootTest(classes = ApplicationStarter.class)
public class DemoServiceImplTest {
@InjectMocks
private DemoService demoService =new DemoServiceImpl();
@BeforeAll
void beforeAllInit() {
System.out.println("running before all");
}
@AfterAll
void afterAllCleanUp() {
System.out.println("running after all");
}
@BeforeEach
void init() {
System.out.println("running before each...");
}
@AfterEach
void cleanUp() {
System.out.println("running after each...");
}
@Test
void testSum() {
assertEquals(2, demoService.addtwoNumbers(1, 1));
}
}
斷言校驗(yàn):
Assertions.assertEquals()值比較校驗(yàn):
assertEquals(expected, actual,message)
里面最少是2個參數(shù),一個自己的期望值「expected
」,一個程序的實(shí)際值「?actual
」。如果想要斷言失敗的情況下顯示自定義的說明,則加上第3個參數(shù),即斷言失敗說明「message
」。
Assertions.assertThrows()異常捕獲校驗(yàn):
assertThrows(Class<T> expectedType, Executable executable, String message)
去判斷代碼拋出的異常是業(yè)務(wù)代碼自定義的異常不,對應(yīng)的期望值變成了異常類型「
Class<T>
」的期望值,實(shí)際的值也是拋出異常的實(shí)際值「Executable
」,同樣如果想要斷言失敗的情況下顯示自定義的說明,則加上第3個參數(shù),即斷言失敗說明「message
」。
代碼示例:
import org.mockito.InjectMocks;
@TestInstance(Lifecycle.PER_CLASS)
@SpringBootTest(classes = ApplicationStarter.class)
public class DemoServiceImplTest {
@InjectMocks
private DemoService demoService =new DemoServiceImpl();
@Test
public void testSum() {
//Assertions.assertThrows()
Exception ex = Assertions.assertThrows(Exception.class, () ->demoService.addtwoNumbers(1, 1))
//Assertions.assertEquals()
Assertions.assertEquals(ex.getMessage(),"test");
}
}
更多詳細(xì)信息參考文檔:test-instance-lifecycle
Mockito使用
在測試代碼塊中經(jīng)常會調(diào)到第三方接口方法(比如第三方SDK接口方法或遠(yuǎn)程RPC接口),涉及數(shù)據(jù)庫操作的接口方法(數(shù)據(jù)庫增刪改查接口)。這些方法需要其他環(huán)境服務(wù)支持,鏈接遠(yuǎn)程數(shù)據(jù)庫,我們只需測試自己編寫的單元代碼塊是否有問題,不想真實(shí)調(diào)用這些方法。要解決這個問題,可以把這些方法都mock(模擬)掉。Mockito框架提供很好的支持。
常用注解
- @Mock:創(chuàng)建一個Mock,用于替換被測試類中的引用的bean或第三方類。
- @InjectMocks:用于創(chuàng)建一個被測試類的實(shí)例,其余用@Mock(或@Spy)注解創(chuàng)建的mock將被注入到用該實(shí)例中。用于被測試類(如service層的ServiceImpl)
- @Mockbean:將Mock對象添加到Spring上下文中。Mock將替換Spring上下文中任何相同類型的現(xiàn)有bean,如果沒有定義相同類型的bean,將添加一個新的bean。如果需要使用Mockbean注解,需要使用SpringRunner(Junit5 中是@ExtendWith(SpringExtension.class)
)
@Autowird 等方式完成自動注入。在單元測試中,沒有啟動 spring 框架,此時就需要通過 @ InjectMocks完成依賴注入。@InjectMocks會將帶有@Spy 和@Mock 注解的對象嘗試注入到被 測試的目標(biāo)類中。如下代碼示例:
代碼示例:
@Component("mock")
public class MockRepository {
public MockData mock(String userName) {
return new MockData(userName);
}
}
import org.mockito.InjectMocks;
@Service
public class DemoServiceImpl {
@Autowired
private UserRepository userRepository;
@Autowired
private ApplicationContext applicationContext;
@Override
public Result getUserInfo(String id) {
User user=userRepository.findUserById(id);
MockRepository mockRepository=applicationContext.getBean("mock");
MockData data=mockRepository.mock(user.getUserName());
return new Result("1000","success",data);
}
}
import org.mockito.InjectMocks;
@TestInstance(Lifecycle.PER_CLASS)
@SpringBootTest(classes = ApplicationStarter.class)
public class DemoServiceImplTest {
@InjectMocks
private DemoService demoService =new DemoServiceImpl();
@Mock
private UserRepository userRepository;
@MockBean
private MockRepository mockRepository;
@Autowired
private ApplicationContext applicationContext;
@Test
public void testSum() {
//Assertions.assertThrows()
Result res =demoService.getUserInfo("test");
//Assertions.assertEquals()
Assertions.assertEquals(res.getCode(),"1000");
}
}
Mock方法:
1.when(...) thenReturn(...)會調(diào)用真實(shí)的方法,如果你不想調(diào)用真實(shí)的方法而是想要mock的話,就不要使用這個方法。
import org.mockito.InjectMocks;
@TestInstance(Lifecycle.PER_CLASS)
@SpringBootTest(classes = ApplicationStarter.class)
public class DemoServiceImplTest {
@InjectMocks
private DemoService demoService =new DemoServiceImpl();
@Mock
private UserRepository userRepository;
@MockBean
private MockRepository mockRepository;
@Autowired
private ApplicationContext applicationContext;
@Test
public void testSum() {
// when(..).thenReturn(..)
Mockito.when(userRepository.findUserById(Mockito.anyString())).thenReturn(new User());
Result res =demoService.getUserInfo("test");
//Assertions.assertEquals()
Assertions.assertEquals(res.getCode(),"1000");
}
}
2.doReturn(...) when(...) 跟when(...) thenReturn(...)一樣都是mock方法,但不會調(diào)用真實(shí)方法。文章來源:http://www.zghlxwxcb.cn/news/detail-781927.html
import org.mockito.InjectMocks;
@TestInstance(Lifecycle.PER_CLASS)
@SpringBootTest(classes = ApplicationStarter.class)
public class DemoServiceImplTest {
@InjectMocks
private DemoService demoService =new DemoServiceImpl();
@Mock
private UserRepository userRepository;
@MockBean
private MockRepository mockRepository;
@Autowired
private ApplicationContext applicationContext;
@Test
public void testSum() {
// doReturn(new User()).when(userRepository)
Mockito.doReturn(new User()).when(userRepository).findUserById(Mockito.anyString()))
Result res =demoService.getUserInfo("test");
//Assertions.assertEquals()
Assertions.assertEquals(res.getCode(),"1000");
}
}
3.doAnswer…when?當(dāng)模擬對象調(diào)用它的方法,需要執(zhí)行一些操作(其實(shí)就是需要執(zhí)行一個代碼塊)才能得到返回值時,則需要使用doAnswer來構(gòu)造產(chǎn)生這個模擬的返回值。例如:當(dāng)模擬對象調(diào)用某個方法的返回值是個復(fù)合值(bean)時,就需要用doAnswer來構(gòu)造該返回值。文章來源地址http://www.zghlxwxcb.cn/news/detail-781927.html
@InjectMocks
private DemoService demoService =new DemoServiceImpl();
@Mock
private StockDao stockDao;
...
@Test
public void stockTest() {
doAnswer(new Answer<StockModel>) {
@Override
public StockModel answer(InvocationOnMock invocation) throws Throwable {
StockModel stock = new StockModel ();
stock.setFundFamilyName("fundFamily01");
return stock;
}
}).when(stockDao).lookup("testStock");
Result res=demoService.stock("testStock");
Assertions.assertEquals(res.getStock(),"test");
}
到了這里,關(guān)于SpringBoot單元測試--Mockito+Junit5框架使用的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!