摘要:但是,一個(gè)好的單元測(cè)試應(yīng)該是毫秒級(jí)的,否則這會(huì)影響的工作方式,這也就是測(cè)試驅(qū)動(dòng)開發(fā)的思想。在單元測(cè)試中,我們可以像這樣來(lái)構(gòu)建一個(gè)實(shí)例。所以,我們?cè)趯憜卧獪y(cè)試的時(shí)候,應(yīng)該以一種更簡(jiǎn)單的方式去構(gòu)建。
本文翻譯自:https://reflectoring.io/unit-...原文作者:Tom Hombergs
譯文原地址:https://weyunx.com/2019/02/04...
寫好單元測(cè)試是一門技術(shù)活,不過(guò)好在我們現(xiàn)在有很多框架來(lái)幫助我們學(xué)習(xí)。
本文就為您介紹這些框架,同時(shí)詳細(xì)介紹編寫優(yōu)秀的 Sping Boot 單元測(cè)試所必需的技術(shù)細(xì)節(jié),
我們將了解如何以可測(cè)試的方式創(chuàng)建 Spring bean,然后討論 Mockito 和 AssertJ 的使用,這兩個(gè)庫(kù)在默認(rèn)情況下都集成在 Spring Boot 里。
需要注意的是本文只討論單元測(cè)試,組裝測(cè)試、web 層測(cè)試和持久層測(cè)試會(huì)在后面的文章里討論。
依賴在本文中,我們將使用 JUnit Jupiter (JUnit 5), Mockito, and AssertJ,同時(shí)還會(huì)引入 Lombok 來(lái)省去一些繁復(fù)的工作。
compileOnly("org.projectlombok:lombok") testCompile("org.springframework.boot:spring-boot-starter-test") testCompile "org.junit.jupiter:junit-jupiter-engine:5.2.0" testCompile("org.mockito:mockito-junit-jupiter:2.23.0")
spring-boot-starter-test 默認(rèn)引入了 Mockito and AssertJ,對(duì)于 Lombok 則需要我們自己手工引入。
不要使用 Spring 進(jìn)行單元測(cè)試看一下下面的「單元」測(cè)試,是用來(lái)測(cè)試 RegisterUseCase 類的一個(gè)方法:
@ExtendWith(SpringExtension.class) @SpringBootTest class RegisterUseCaseTest { @Autowired private RegisterUseCase registerUseCase; @Test void savedUserHasRegistrationDate() { User user = new User("zaphod", "zaphod@mail.com"); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); } }
我們?nèi)?zhí)行這個(gè)測(cè)試類,花了大概 4.5 秒的時(shí)間,原因僅僅是因?yàn)橛?jì)算機(jī)要為它去運(yùn)行一個(gè)空的 Spring 項(xiàng)目。
但是,一個(gè)好的單元測(cè)試應(yīng)該是毫秒級(jí)的,否則這會(huì)影響「test / code / test」的工作方式,這也就是測(cè)試驅(qū)動(dòng)開發(fā)的思想 (TDD)。即使我們不做 TDD,在編寫測(cè)試上花了太多時(shí)間也會(huì)影響我們的開發(fā)思路。
其實(shí),上面的測(cè)試方法實(shí)際執(zhí)行只花費(fèi)了幾毫秒,剩下的 4.5 秒全部花費(fèi)在了 @SpringBootRun 上,因?yàn)?Spring Boot 需要啟動(dòng)整個(gè) Spring Boot 應(yīng)用。
也就是說(shuō),我們啟動(dòng)整個(gè)應(yīng)用,耗費(fèi)了大量資源,僅僅是去為了測(cè)試一個(gè)方法,當(dāng)我們的應(yīng)用未來(lái)越來(lái)越大的時(shí)候,那將耗費(fèi)更久的時(shí)間去啟動(dòng)。
所以,為什么不要用 Spring Boot 來(lái)做單元測(cè)試呢?接下來(lái),本文會(huì)討論如何不用 Spring Boot 來(lái)進(jìn)行單元測(cè)試。
創(chuàng)建測(cè)試類通常,我們可以有如下方法來(lái)讓我們的 Spring beans 更容易進(jìn)行測(cè)試。
不要注入首先我們先看一個(gè)錯(cuò)誤的例子:
@Service public class RegisterUseCase { @Autowired private UserRepository userRepository; public User registerUser(User user) { return userRepository.save(user); } }
然而這個(gè)類還是必須通過(guò) Spring 才能執(zhí)行,因?yàn)槲覀儫o(wú)法繞過(guò) UserRepository 這個(gè)實(shí)例。就像前面提到的,我們必須換一種方法,不使用 @Autowired 來(lái)注入 UserRepository。
知識(shí)點(diǎn):不要注入
寫一個(gè)構(gòu)造器我們看一下不使用 @Autowired 的寫法:
@Service public class RegisterUseCase { private final UserRepository userRepository; public RegisterUseCase(UserRepository userRepository) { this.userRepository = userRepository; } public User registerUser(User user) { return userRepository.save(user); } }
這個(gè)版本使用構(gòu)造器來(lái)引入 UserRepository 實(shí)例。在單元測(cè)試中,我們可以像這樣來(lái)構(gòu)建一個(gè)實(shí)例。
Spring 會(huì)自動(dòng)的使用構(gòu)造器來(lái)實(shí)例化一個(gè) RegisterUseCase 對(duì)象。需要注意的是,在 Spring 5 之前,我們需要@Autowired 注解來(lái)讓構(gòu)造器生效。
同樣需要注意的是 UserRepository 字段現(xiàn)在是 final,這樣在整個(gè)應(yīng)用的生命周期里,它都將是個(gè)常量,這可以避免編碼錯(cuò)誤,因?yàn)槲覀內(nèi)绻洺跏蓟侄?,編譯的時(shí)候就會(huì)報(bào)錯(cuò)。
減少繁復(fù)的代碼使用 Lombok 的 @RequiredArgsConstructor 注解,可以讓構(gòu)造器的寫法更簡(jiǎn)潔:
@Service @RequiredArgsConstructor public class RegisterUseCase { private final UserRepository userRepository; public User registerUser(User user) { user.setRegistrationDate(LocalDateTime.now()); return userRepository.save(user); } }
現(xiàn)在我們的測(cè)試類就很簡(jiǎn)潔,沒有冗余繁復(fù)的代碼:
class RegisterUseCaseTest { private UserRepository userRepository = ...; private RegisterUseCase registerUseCase; @BeforeEach void initUseCase() { registerUseCase = new RegisterUseCase(userRepository); } @Test void savedUserHasRegistrationDate() { User user = new User("zaphod", "zaphod@mail.com"); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); } }
不過(guò)我們還有一點(diǎn)遺漏,就是如何去模擬 UserRepository 實(shí)例,因?yàn)槲覀儾幌肴フ嬲娜?zhí)行,因?yàn)樗赡苄枰ミB接數(shù)據(jù)庫(kù)。
使用 Mockito現(xiàn)行的標(biāo)準(zhǔn)模擬庫(kù)是 Mockito,它提供了至少兩種方式來(lái)模擬 UserRepository 。
直接調(diào)用第一種方法就是直接使用 Mockito:
private UserRepository userRepository = Mockito.mock(UserRepository.class);
這個(gè)創(chuàng)建一個(gè)對(duì)象,看起來(lái)和 UserRepository 一樣。默認(rèn)的情況下,這個(gè)類什么也不會(huì)做,如果調(diào)用有返回值的方法,也只會(huì)返回 null。
我們的測(cè)試現(xiàn)在會(huì)是失敗,在 assertThat(savedUser.getRegistrationDate()).isNotNull() 這兒報(bào) NullPointerException 空指針異常,因?yàn)?userRepository.save(user) 只會(huì)返回 null。
所以,我們需要告訴 Mockito,當(dāng) userRepository.save() 被調(diào)用的時(shí)候需要有返回值,所以我們使用靜態(tài)的 when 方法:
@Test void savedUserHasRegistrationDate() { User user = new User("zaphod", "zaphod@mail.com"); when(userRepository.save(any(User.class))).then(returnsFirstArg()); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); }
這樣 userRepository.save() 會(huì)返回一個(gè)對(duì)象,其實(shí)這個(gè)對(duì)象和傳入?yún)?shù)的對(duì)象一摸一樣。
Mockito 具有一整套的測(cè)試方案,可以用來(lái)模擬、匹配參數(shù)以及識(shí)別方法的調(diào)用,更多資料可以參考這里。
使用 @Mock此外還可以用 @Mock 注解來(lái)模擬對(duì)象,它需要和 MockitoExtension 組合使用。
@ExtendWith(MockitoExtension.class) class RegisterUseCaseTest { @Mock private UserRepository userRepository; private RegisterUseCase registerUseCase; @BeforeEach void initUseCase() { registerUseCase = new RegisterUseCase(userRepository); } @Test void savedUserHasRegistrationDate() { // ... } }
@Mock 注解會(huì)指定字段將被注入到 mock 對(duì)象,@MockitoExtension 會(huì)告訴 Mockito 去掃描 @Mock 注解,因?yàn)?JUnit 不會(huì)自動(dòng)去執(zhí)行。
這其實(shí)和直接手工執(zhí)行 Mockito.mock() 的結(jié)果一樣,只是使用習(xí)慣的區(qū)別。不過(guò)使用 MockitoExtension 我們的測(cè)試就可以綁定到測(cè)試框架里。
需要說(shuō)明的是我們可以在 registerUseCase 字段上使用 @InjectMocks 注解來(lái)替代手工構(gòu)造一個(gè) RegisterUseCase 對(duì)象,Mockito 會(huì)幫我們自動(dòng)構(gòu)造對(duì)象,如:
@ExtendWith(MockitoExtension.class) class RegisterUseCaseTest { @Mock private UserRepository userRepository; @InjectMocks private RegisterUseCase registerUseCase; @Test void savedUserHasRegistrationDate() { // ... } }讓斷言更直白
另一個(gè) Spring Boot 自帶的測(cè)試支持庫(kù)是 AssertJ,上面的例子里,在實(shí)現(xiàn)斷言的時(shí)候已經(jīng)用到了:
assertThat(savedUser.getRegistrationDate()).isNotNull();
不過(guò)我們想讓寫法變得更直白好理解,比如:
assertThat(savedUser).hasRegistrationDate();
通常,我們可以做小改動(dòng)就可以讓代碼變得更容易理解,所以我們新建一個(gè)自定義的斷言對(duì)象:
public class UserAssert extends AbstractAssert{ public UserAssert(User user) { super(user, UserAssert.class); } public static UserAssert assertThat(User actual) { return new UserAssert(actual); } public UserAssert hasRegistrationDate() { isNotNull(); if (actual.getRegistrationDate() == null) { failWithMessage("Expected user to have a registration date, but it was null"); } return this; } }
這樣,我們調(diào)用 UserAssert 類的 assertThat 方法,而不是直接從 Assertj 庫(kù)里調(diào)用。
創(chuàng)建自定義的斷言看起來(lái)需要很多的工作量,但其實(shí)也就是幾分鐘的事。我相信這幾分鐘的工作,絕對(duì)是值得的,即使是讓代碼看起來(lái)更直白容易理解。測(cè)試代碼我們只會(huì)寫一次,然后其他人(包括我在以后)都只是去讀這段代碼,然后是反反復(fù)復(fù)的去修改這段代碼,直到產(chǎn)品消亡。
如果還有疑問(wèn),可以參考 Assertions Generator。
結(jié)論我們可能有種種的理由在 Spring 里進(jìn)行測(cè)試,但是對(duì)于一個(gè)普通的單元測(cè)試,可以這么做,但是沒有必要。隨著以后應(yīng)用越來(lái)越龐大,啟動(dòng)時(shí)間越來(lái)越長(zhǎng),可能還會(huì)帶來(lái)問(wèn)題。所以,我們?cè)趯憜卧獪y(cè)試的時(shí)候,應(yīng)該以一種更簡(jiǎn)單的方式去構(gòu)建 Sprnig bean。
Spring Boot Test Starter 附帶了 Mockito 和 AssertJ 作為測(cè)試依賴庫(kù),所以盡可能的使用這些測(cè)試庫(kù)來(lái)做更好的單元測(cè)試吧。
所有的代碼可以在這里找到。
如果發(fā)現(xiàn)譯文存在錯(cuò)誤或其他需要改進(jìn)的地方,歡迎斧正。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/73468.html
摘要:在容器領(lǐng)域內(nèi),已毋庸置疑成為了容器編排和管理的社區(qū)標(biāo)準(zhǔn)??蛻舳藷o(wú)需連接到每個(gè)的,而是直接連接負(fù)載均衡器的地址。通過(guò)這樣的操作,使用持續(xù)交付和部署方法論的快速開發(fā)和部署周期將會(huì)成為常態(tài)。 在容器領(lǐng)域內(nèi),Kubernetes已毋庸置疑成為了容器編排和管理的社區(qū)標(biāo)準(zhǔn)。如果你希望你所搭建的應(yīng)用程序能充分利用多云(multi-cloud)的優(yōu)勢(shì),有一些與Kubernetes網(wǎng)絡(luò)相關(guān)的基本內(nèi)容是你...
摘要:在容器領(lǐng)域內(nèi),已毋庸置疑成為了容器編排和管理的社區(qū)標(biāo)準(zhǔn)??蛻舳藷o(wú)需連接到每個(gè)的,而是直接連接負(fù)載均衡器的地址。通過(guò)這樣的操作,使用持續(xù)交付和部署方法論的快速開發(fā)和部署周期將會(huì)成為常態(tài)。 在容器領(lǐng)域內(nèi),Kubernetes已毋庸置疑成為了容器編排和管理的社區(qū)標(biāo)準(zhǔn)。如果你希望你所搭建的應(yīng)用程序能充分利用多云(multi-cloud)的優(yōu)勢(shì),有一些與Kubernetes網(wǎng)絡(luò)相關(guān)的基本內(nèi)容是你...
摘要:在容器領(lǐng)域內(nèi),已毋庸置疑成為了容器編排和管理的社區(qū)標(biāo)準(zhǔn)。客戶端無(wú)需連接到每個(gè)的,而是直接連接負(fù)載均衡器的地址。通過(guò)這樣的操作,使用持續(xù)交付和部署方法論的快速開發(fā)和部署周期將會(huì)成為常態(tài)。 在容器領(lǐng)域內(nèi),Kubernetes已毋庸置疑成為了容器編排和管理的社區(qū)標(biāo)準(zhǔn)。如果你希望你所搭建的應(yīng)用程序能充分利用多云(multi-cloud)的優(yōu)勢(shì),有一些與Kubernetes網(wǎng)絡(luò)相關(guān)的基本內(nèi)容是你...
閱讀 1279·2021-10-11 10:59
閱讀 2032·2021-09-29 09:44
閱讀 947·2021-09-01 10:32
閱讀 1494·2019-08-30 14:21
閱讀 1932·2019-08-29 15:39
閱讀 3037·2019-08-29 13:45
閱讀 3601·2019-08-29 13:27
閱讀 2071·2019-08-29 12:27