title: 最佳單元測(cè)試實(shí)踐
date: 2021/09/08 15:11
注:本文使用的是 SpringBootTest2.x + Junit4 + Mockito,本文的前提是你已經(jīng)會(huì)使用這些工具了乐导。
引言:常見(jiàn)單元測(cè)試方法
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class) // 1
@Transactional // 2
@Rollback
public class HelloServiceTest {
@Autowired
private HelloService helloService;
@Test
public void sayHello() {
helloService.sayHello("zhangsan"); // 3
}
}
這樣的寫(xiě)法不符合規(guī)范的地方如下:
- 使用
@SpringBootTest
注解將主啟動(dòng)類(lèi) Application 的 BeanDefintion 加入到了 Spring 容器中圃泡,從而加載了所有的 Bean,導(dǎo)致大量的時(shí)間耗費(fèi)在于容器啟動(dòng)上祖很。而且笛丙,如果被 @Component 注解的類(lèi)里有多線程方法,那么在你執(zhí)行單元測(cè)試的時(shí)候假颇,由于多線程任務(wù)的影響胚鸯,就可能對(duì)你的數(shù)據(jù)庫(kù)造成了數(shù)據(jù)修改,即使你使用了事務(wù)回滾注解 @Transactional笨鸡。
- eg. 在我運(yùn)行單元測(cè)試的時(shí)候姜钳,代碼里的其他類(lèi)的多線程中不停接收activeMQ消息,然后更新數(shù)據(jù)庫(kù)中對(duì)應(yīng)的數(shù)據(jù)镜豹。跟單元測(cè)試的執(zhí)行過(guò)程交叉重疊傲须,導(dǎo)致單元測(cè)試失敗。其他組員在操作數(shù)據(jù)庫(kù)的時(shí)候趟脂,也因?yàn)槲覠o(wú)意中帶起的多線程更改了數(shù)據(jù)庫(kù)泰讽,造成了開(kāi)發(fā)上的困難。
- 單元測(cè)試應(yīng)與數(shù)據(jù)庫(kù)完全隔離(不應(yīng)受到外界環(huán)境的影響昔期,違反了可重復(fù)的原則)已卸,數(shù)據(jù)庫(kù)相關(guān)操作應(yīng)使用 mock 代替。
- 沒(méi)有使用斷言(assert)硼一,無(wú)法實(shí)現(xiàn)自動(dòng)化判斷累澡。
一、單元測(cè)試的原則
1.1 AIR 原則
- Automatic(自動(dòng)化的):自動(dòng)通過(guò)一系列的斷言給出執(zhí)行結(jié)果般贼,而不需要人為去判斷愧哟,在幾十上百的測(cè)試用例下很難人為的去判斷。
- Independent(獨(dú)立的):測(cè)試用例之間不能相互依賴影響哼蛆,是獨(dú)立的
- Repeatable(可重復(fù)的):?jiǎn)卧獪y(cè)試是可以重復(fù)執(zhí)行的蕊梧,不能受到外界環(huán)境的影響,如數(shù)據(jù)庫(kù)腮介、遠(yuǎn)程調(diào)用肥矢、中間件等外部依賴不能影響測(cè)試用例的執(zhí)行。
1.2 BCDE 原則
保證被測(cè)試模塊的交付質(zhì)量叠洗。
- B:Border甘改,邊界值測(cè)試旅东,包括循環(huán)邊界、特殊取值十艾、特殊時(shí)間點(diǎn)抵代、數(shù)據(jù)順序等。
- C:Correct疟羹,正確的輸入主守,并得到預(yù)期的結(jié)果。
- D:Design榄融,與設(shè)計(jì)文檔相結(jié)合参淫,來(lái)編寫(xiě)單元測(cè)試。
- E:Error愧杯,強(qiáng)制錯(cuò)誤信息輸入(如:非法數(shù)據(jù)涎才、異常流程、業(yè)務(wù)允許外等)力九,并得到預(yù)期的結(jié)果耍铜。
1.3 使用 Mock 對(duì)象
Mock 可以用來(lái)解除外部服務(wù)依賴,從而保證了測(cè)試用例的獨(dú)立性
-
Mock 可以減少全鏈路測(cè)試數(shù)據(jù)準(zhǔn)備跌前,從而提高了編寫(xiě)測(cè)試用例的速度
傳統(tǒng)的集成測(cè)試棕兼,需要準(zhǔn)備全鏈路的測(cè)試數(shù)據(jù),可能某些環(huán)節(jié)并不是你所熟悉的抵乓。最后伴挚,耗費(fèi)了大量的時(shí)間和經(jīng)歷,并不一定得到你想要的結(jié)果≡痔浚現(xiàn)在的單元測(cè)試茎芋,只需要模擬上游的輸入數(shù)據(jù),并驗(yàn)證給下游的輸出數(shù)據(jù)蜈出,編寫(xiě)測(cè)試用例并進(jìn)行測(cè)試的速度可以提高很多倍田弥。
-
Mock可以模擬一些非正常的流程,從而保證了測(cè)試用例的代碼覆蓋率
根據(jù)單元測(cè)試的BCDE原則铡原,需要進(jìn)行邊界值測(cè)試(Border)和強(qiáng)制錯(cuò)誤信息輸入(Error)偷厦,這樣有助于覆蓋整個(gè)代碼邏輯。在實(shí)際系統(tǒng)中燕刻,很難去構(gòu)造這些邊界值沪哺,也能難去觸發(fā)這些錯(cuò)誤信息。而 Mock 從根本上解決了這個(gè)問(wèn)題:想要什么樣的邊界值酌儒,只需要進(jìn)行Mock;想要什么樣的錯(cuò)誤信息枯途,也只需要進(jìn)行Mock忌怎。
Mock可以不用加載項(xiàng)目環(huán)境配置籍滴,從而保證了測(cè)試用例的執(zhí)行速度
在進(jìn)行集成測(cè)試時(shí),我們需要加載項(xiàng)目的所有環(huán)境配置榴啸,啟動(dòng)項(xiàng)目依賴的所有服務(wù)接口孽惰。往往執(zhí)行一個(gè)測(cè)試用例,需要幾分鐘乃至幾十分鐘鸥印。采用Mock實(shí)現(xiàn)的測(cè)試用例勋功,不用加載項(xiàng)目環(huán)境配置,也不依賴其它服務(wù)接口库说,執(zhí)行速度往往在幾秒之內(nèi)狂鞋,大大地提高了單元測(cè)試的執(zhí)行速度。
什么是集成測(cè)試潜的?
集成測(cè)試是指在單元測(cè)試的基礎(chǔ)上骚揍,將所有模塊(或單元)按照設(shè)計(jì)要求(如根據(jù)結(jié)構(gòu)圖)組裝成為子系統(tǒng)或系統(tǒng),進(jìn)行集成測(cè)試啰挪。
實(shí)踐表明信不,一些模塊雖然能夠單獨(dú)地工作,但并不能保證連接起來(lái)也能正常的工作亡呵。 一些局部反映不出來(lái)的問(wèn)題抽活,在全局上很可能暴露出來(lái)。
由于集成測(cè)試的單位是一整個(gè)系統(tǒng)锰什,一般有專(zhuān)業(yè)的測(cè)試人員來(lái)進(jìn)行下硕,本文只做簡(jiǎn)單介紹,不繼續(xù)探討歇由。
二卵牍、最佳實(shí)踐
寫(xiě)好單元測(cè)試,以下兩點(diǎn)尤為重要:
- 使用 Mock 脫離數(shù)據(jù)庫(kù)
-
不使用
@SpringBootTest
注解加載全部 BeanDefinition沦泌,轉(zhuǎn)而使用@ContextConfiguration
注解加載需要的配置類(lèi)
2.1 使用 Mock 代替數(shù)據(jù)庫(kù)
mockito 為 Junit 提供了MockitoJUnitRunner
用于解析單元類(lèi)中 mockito 相關(guān)注解糊昙。當(dāng)然使用 SpringRunner 也行,因?yàn)樗麅?nèi)置了 MockitoTestExecutionListener 來(lái)處理 mockito 的注解谢谦。
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.List;
@RunWith(MockitoJUnitRunner.class)
// 使用 SpringRunner 也行释牺,因?yàn)樗麅?nèi)置了 MockitoTestExecutionListener 來(lái)處理 mockito 的注解
// @RunWith(SpringRunner.class)
public class MockitoTest {
/**
* 如 UserService 是接口,則需 new 出他的實(shí)現(xiàn)類(lèi)回挽,如下:
*
* <pre> {@code
* @InjectMocks
* private UserService userService = new UserServiceImpl();
* }</pre>
*
* 否則使用 @InjectMocks 注解無(wú)法注入
*/
@InjectMocks
private UserService userService;
@Mock
private UserRepository userRepository;
@Test
public void test() {
// 模擬依賴方法
Mockito.when(userRepository.findAll())
.thenReturn(Arrays.asList(new User(1, "zs"), new User(2, "ls")));
// 調(diào)用被測(cè)方法
List<User> users = userService.listAll();
// 斷言方法結(jié)果
Assert.assertEquals(2, users.size());
// 驗(yàn)證依賴方法
// 是否只調(diào)用了一次 findAll() 方法
Mockito.verify(userRepository).findAll();
// 是否與 userRepository 對(duì)象再無(wú)交互
Mockito.verifyNoMoreInteractions(userRepository);
}
}
注:如對(duì)象較大没咙,則可在類(lèi)路徑下存放 json 文件,通過(guò) Json 工具將其序列化成對(duì)象千劈。
Junit5 版祭刚,注意 Test 注解的包名與上面不同
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.List;
@ExtendWith(MockitoExtension.class)
//@ExtendWith(SpringExtension.class)
public class MockitoTest {
@InjectMocks
private UserService userService;
@Mock
private UserRepository userRepository;
@Test
public void test() {
// 模擬依賴方法
Mockito.when(userRepository.findAll())
.thenReturn(Arrays.asList(new User(1, "zs"), new User(2, "ls")));
// 調(diào)用被測(cè)方法
List<User> users = userService.listAll();
// 斷言方法結(jié)果
Assertions.assertEquals(2, users.size());
// 驗(yàn)證依賴方法
// 是否只調(diào)用了一次 findAll() 方法
Mockito.verify(userRepository).findAll();
// 是否與 userRepository 對(duì)象再無(wú)交互
Mockito.verifyNoMoreInteractions(userRepository);
}
}
如果一個(gè)方法的調(diào)用鏈路如下:
Controller -> Service -> Repo
,那么應(yīng)該將其拆分成兩個(gè)單元來(lái)測(cè)試:
- TestController + mockService
- TestService -> mockRepo
如果在測(cè)試
Controller
的時(shí) mock 了Repo
(TestController + @Autowired Service + mockRepo),這樣這就不能叫做單元測(cè)試了涡驮。單元測(cè)試只保證每個(gè)單元能夠單獨(dú)地工作暗甥,但并不能保證連接起來(lái)也能正常的工作;上面這種多個(gè)跨了多個(gè)單元的應(yīng)該使用集成測(cè)試捉捅。
2.2 只加載需要的 Bean
上面的寫(xiě)法只適用于不使用 Spring 給我們提供的功能情況下撤防,但往往有的時(shí)候我們需要他們給我們提供的功能,就比如通過(guò)@Async
注解啟動(dòng)異步任務(wù)棒口。那么這種情況我們要怎樣做單元測(cè)試呢寄月?讓我們回到?jīng)]有 SpringBoot 的時(shí)代,看看 Spring Test 是怎樣進(jìn)行單元測(cè)試的无牵。
Spring Test 為我們提供了@ContextConfiguration
注解漾肮,該注解可以加載 Spring 的 xml 配置文件和配置類(lèi),使用方式如下:
被測(cè)試 bean:
@Component
public class AsyncService {
private static final Logger log = LoggerFactory.getLogger(AsyncService.class);
@Autowired
private FeginClient feginClient;
@Async
public Future<Integer> startTask(String taskInstanceId) {
log.info("taskInstanceId:「{}」", taskInstanceId);
return new AsyncResult<>(feginClient.calc(taskInstanceId));
}
}
單元測(cè)試:
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.ExecutionException;
@RunWith(SpringRunner.class)
// 只引入需要的 bean
@ContextConfiguration(classes = {
// 被測(cè) bean
AsyncService.class,
// 開(kāi)啟異步注解解析
SpringTest2.AsyncTestConfig.class,
// 線程池默認(rèn)的自動(dòng)配置類(lèi)合敦,如果自定義了則替換成自定義的配置類(lèi)
TaskExecutionAutoConfiguration.class
})
public class SpringTest2 {
/**
* 由于需要使用 Spring 提供的異步功能初橘,故需要使用 Spring 提供的 Mock 注解
*/
@MockBean
private FeginClient feginClient;
/**
* AsyncService 中依賴了 FeginClient,會(huì)將上面 mock 的對(duì)象進(jìn)行 DI
*/
@Autowired
private AsyncService asyncService;
@Test
public void testCalc() throws ExecutionException, InterruptedException {
// 模擬依賴方法
Mockito.when(feginClient.calc(ArgumentMatchers.anyString())).thenReturn(1);
// 調(diào)用被測(cè)方法
Integer result = asyncService.startTask("1").get();
// 斷言方法結(jié)果
Assert.assertEquals(1, result.intValue());
// 驗(yàn)證依賴方法
Mockito.verify(feginClient).calc(ArgumentMatchers.anyString());
Mockito.verifyNoMoreInteractions(feginClient);
}
@EnableAsync
@Configuration
public static class AsyncTestConfig {
}
}
都寫(xiě)的挺好的充岛,按照順序看一下:
https://www.cnblogs.com/myitnews/p/12330297.html
https://fanlychie.github.io/post/spring-boot-testing.html
https://blog.51cto.com/codewalker/4375122
單元測(cè)試:
- spring test & junit
- @ContextConfiguration保檐、@RunWith(SpringRunner.class)、@Test
切面測(cè)試(啟動(dòng)一部分組件):
- spring-boot-test-autoconfig
- @* Test 系列注解
@DataRedisTest:該注解用于測(cè)試對(duì)Redis操作崔梗,自動(dòng)掃描被@RedisHash描述的類(lèi)夜只,并配置Spring Data Redis的庫(kù)。該注解會(huì)啟動(dòng)一個(gè)內(nèi)存中的Redis服務(wù)器蒜魄,并使用隨機(jī)端口進(jìn)行監(jiān)聽(tīng)扔亥,同時(shí)自動(dòng)配置RedisTemplate和StringRedisTemplate等bean,以便我們可以輕松地執(zhí)行Redis操作谈为。
@DataJpaTest:該注解用于測(cè)試基于JPA的數(shù)據(jù)庫(kù)操作旅挤,同時(shí)提供了TestEntityManager替代JPA的EntityManager。該注解會(huì)用嵌入式的數(shù)據(jù)庫(kù)(例如H2)創(chuàng)建一個(gè)測(cè)試環(huán)境伞鲫,同時(shí)自動(dòng)配置EntityManagerFactory粘茄、DataSource、TransactionManager等bean秕脓,以便我們可以輕松地測(cè)試JPA實(shí)體和Repository層代碼柒瓣。
@DataJdbcTest:該注解用于測(cè)試基于Spring Data JDBC的數(shù)據(jù)庫(kù)操作。該注解會(huì)用嵌入式的數(shù)據(jù)庫(kù)(例如H2)創(chuàng)建一個(gè)測(cè)試環(huán)境吠架,并自動(dòng)配置JdbcTemplate芙贫、NamedParameterJdbcTemplate等bean,以便我們可以輕松地測(cè)試JDBC操作傍药。
@JsonTest:該注解用于測(cè)試JSON的序列化和反序列化磺平。該注解會(huì)自動(dòng)配置Jackson ObjectMapper bean魂仍,并提供了一些輔助方法,方便我們進(jìn)行JSON序列化和反序列化操作褪秀。
@WebMvcTest:該注解用于測(cè)試Spring MVC中的controllers蓄诽。該注解會(huì)自動(dòng)配置MockMvc bean,并提供了一些輔助方法媒吗,方便我們進(jìn)行controller層的測(cè)試操作。
@WebFluxTest:該注解用于測(cè)試Spring WebFlux中的controllers乙埃。該注解會(huì)自動(dòng)配置WebTestClient bean闸英,并提供了一些輔助方法,方便我們進(jìn)行WebFlux相關(guān)的測(cè)試操作介袜。
@RestClientTest:該注解用于測(cè)試對(duì)REST客戶端的操作甫何。該注解會(huì)自動(dòng)配置RestTemplate bean,并提供了一些輔助方法遇伞,方便我們進(jìn)行REST客戶端相關(guān)的測(cè)試操作辙喂。
@DataLdapTest:該注解用于測(cè)試對(duì)LDAP的操作。該注解會(huì)使用嵌入式的LDAP服務(wù)器創(chuàng)建一個(gè)測(cè)試環(huán)境鸠珠,并自動(dòng)配置LdapTemplate bean和EmbeddedLdap bean巍耗,以便我們可以輕松地測(cè)試LDAP操作。
@DataMongoTest:該注解用于測(cè)試對(duì)MongoDB的操作渐排。該注解會(huì)用嵌入式的MongoDB服務(wù)器創(chuàng)建一個(gè)測(cè)試環(huán)境炬太,并自動(dòng)配置MongoTemplate、MongoClient等bean驯耻,以便我們可以輕松地測(cè)試MongoDB操作亲族。
@DataNeo4jTest:該注解用于測(cè)試對(duì)Neo4j的操作。該注解會(huì)使用嵌入式的Neo4j服務(wù)器創(chuàng)建一個(gè)測(cè)試環(huán)境可缚,并自動(dòng)配置Neo4jTemplate霎迫、Neo4jClient等bean,以便我們可以輕松地測(cè)試Neo4j操作帘靡。
功能測(cè)試(啟動(dòng)全部容器):@SpringBootTest
契約測(cè)試:todo
import com.dist.xdata.dgpm.dao.TopicCatalogRepo;
import com.dist.xdata.dgpm.entity.TopicCatalog;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.persistence.EntityManager;
import java.util.List;
/**
* @author yujx
* @since 2023/6/13 4:53 PM
*/
@DataJpaTest
@ExtendWith(SpringExtension.class)
public class TestDataJpa {
@Autowired
private EntityManager entityManager;
@Autowired
private TestEntityManager testEntityManager;
@Autowired
private TopicCatalogRepo repo;
@Test
public void test() {
System.out.println("entityManager = " + entityManager);
System.out.println("testEntityManager = " + testEntityManager);
TopicCatalog save = repo.save(RandomUtil.nextObject(TopicCatalog.class));
System.out.println("save = " + save);
List<TopicCatalog> all = repo.findAll();
System.out.println("all = " + all);
}
}
https://raw.githubusercontent.com/x54256/pic_home/master/img/202306131738520.png