應(yīng)用程序在分發(fā)之前應(yīng)該經(jīng)過(guò)測(cè)試和驗(yàn)證。測(cè)試的目的是驗(yàn)證應(yīng)用程序是否符合功能和非功能要求利虫,并檢測(cè)應(yīng)用程序中的錯(cuò)誤俘陷。
TDD:測(cè)試驅(qū)動(dòng)開(kāi)發(fā)
一旦需求和規(guī)范得到驗(yàn)證,就可以開(kāi)始一個(gè)稱(chēng)為測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的過(guò)程愿待。您首先編寫(xiě)測(cè)試浩螺,然后開(kāi)發(fā)代碼。
將根據(jù)商定的要求和規(guī)范創(chuàng)建測(cè)試(測(cè)試評(píng)審方案)呼盆;最初測(cè)試會(huì)失敗年扩,我們將在應(yīng)用程序中編寫(xiě)代碼以確保測(cè)試通過(guò)。一旦測(cè)試通過(guò)访圃,我們可以重構(gòu)應(yīng)用程序中的代碼以改進(jìn)它并再次啟動(dòng)測(cè)試厨幻。
此類(lèi)測(cè)試應(yīng)由分析師設(shè)計(jì)并由開(kāi)發(fā)人員實(shí)施。如果我們注意到某個(gè)規(guī)范的測(cè)試很難開(kāi)發(fā),我們應(yīng)該考慮這樣一個(gè)事實(shí)况脆,即該規(guī)范可能不正確或至少是不明確的饭宾。
多虧了 TDD 技術(shù),我們可以在開(kāi)發(fā)的早期階段發(fā)現(xiàn)任何問(wèn)題格了】疵考慮到解決問(wèn)題的努力與找到問(wèn)題所需的時(shí)間成正比。
單元和集成測(cè)試
單元測(cè)試作為類(lèi)方法驗(yàn)證應(yīng)用程序的一小部分的功能盛末,并且獨(dú)立于應(yīng)用程序的其他單元并隔離弹惦。
好的公司,單元測(cè)試都是在開(kāi)發(fā)完畢后悄但,自己就進(jìn)行處理完成了棠隐。當(dāng)然測(cè)試人員不是不可以做,主要是側(cè)重點(diǎn)不同檐嚣。
我們可以將各個(gè)單元視為應(yīng)用程序的各個(gè)層助泽。
因此,一個(gè)好的單元測(cè)試獨(dú)立于整個(gè)應(yīng)用程序基礎(chǔ)設(shè)施嚎京,如數(shù)據(jù)庫(kù)類(lèi)型和其他層嗡贺。如果測(cè)試方法與其他單元有依賴(lài)關(guān)系,它們可以被模擬(可能使用像 Mockito 這樣的庫(kù))鞍帝。在我們將要做的示例中诫睬,我們將測(cè)試一個(gè) Service 方法,并且該測(cè)試將獨(dú)立于數(shù)據(jù)庫(kù)和 Spring 的上下文(因此該測(cè)試適用于任何用于依賴(lài)注入的框架)膜眠。
集成測(cè)試驗(yàn)證應(yīng)用程序的多個(gè)單元的操作岩臣。這里使用的框架的上下文也用于測(cè)試階段。
需求示例
根據(jù)需求宵膨,我們被要求實(shí)現(xiàn) findById 的 REST API 并創(chuàng)建一個(gè) User 模型架谎。要求產(chǎn)品經(jīng)理提供明確的需求,開(kāi)發(fā)去實(shí)現(xiàn)產(chǎn)品提供的需求辟躏。
成功返回:
特別是谷扣,在 findById 中,客戶(hù)端必須收到 200 并且在響應(yīng)正文中收到找到的用戶(hù)的 JSON捎琐。
失敗或者不存在返回:
如果沒(méi)有具有輸入 id 的用戶(hù)会涎,則客戶(hù)端必須收到帶有空正文的 404。
此外瑞凑,客戶(hù)端會(huì)看到用戶(hù)的 2 個(gè)字段:姓名和地址末秃;名稱(chēng)由姓氏、空格和名字組成籽御。
該項(xiàng)目將具有以下層:
實(shí)體將包含具有姓名练慕、姓氏和地址的實(shí)體用戶(hù)
dtos 將包含映射實(shí)體 User 的 DTO UserDTO惰匙,帶有字段名稱(chēng)(“姓氏 + 名字”)和地址
負(fù)責(zé)將 User 轉(zhuǎn)換為 UserDTO 的轉(zhuǎn)換器,反之亦然
存儲(chǔ)庫(kù)將包含實(shí)體用戶(hù)的 Spring JpaRepository
將包含 UserService 的服務(wù)铃将,該服務(wù)將從數(shù)據(jù)庫(kù)中輕松檢索用戶(hù)并使用轉(zhuǎn)換器將其轉(zhuǎn)換為 DTO
將包含 UserController 的控制器项鬼,它負(fù)責(zé)映射 REST 調(diào)用并使用服務(wù)的業(yè)務(wù)邏輯。
第一步:讓我們創(chuàng)建實(shí)體和 dto
@Entity
public class User implements Serializable {
? ? @Id
? ? @GeneratedValue(strategy = GenerationType.IDENTITY)
? ? private Long id;
? ? private String name;
? ? private String surname;
? ? private String address;
? ? public User() {}
? ? public User(String name, String surname, String address) {
? ? ? ? this.name = name;
? ? ? ? this.surname = surname;
? ? ? ? this.address = address;
? ? }
? ? //getter, setter, equals and hashcode
}
@Entity
public class UserDTO {
? ? //surname + name
? ? private String name;
? ? private String address;
? ? public UserDTO() {}
? ? public UserDTO(String name, String address) {
? ? ? ? this.name = name;
? ? ? ? this.address = address;
? ? }
? ? //getter, setter, equals and hashcode
}
第二步:讓我們創(chuàng)建轉(zhuǎn)換器
@Component
public class UserConverter {
? ? public UserDTO userToUserDTO(User user) {
? ? ? ? return new UserDTO(user.getSurname() + " " + user.getName(), user.getAddress());
? ? }
? ? public User userDTOToUser(UserDTO userDTO) {
? ? ? ? String[] surnameAndName = userDTO.getName().split(" ");
? ? ? ? return new User(surnameAndName[1], surnameAndName[0], userDTO.getAddress());
? ? }
}
第三步:讓我們?yōu)?User 實(shí)體創(chuàng)建一個(gè)存儲(chǔ)庫(kù)
public interface UserRepository extends JpaRepository<User,Long> {
}
第四步:讓我們用 findById 方法創(chuàng)建服務(wù)
@Service
public class UserServiceImpl implements UserService {
? ? private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
? ? private UserRepository userRepository;
? ? private UserConverter userConverter;
? ? public UserServiceImpl(UserRepository userRepository, UserConverter userConverter) {
? ? ? ? this.userRepository = userRepository;
? ? ? ? this.userConverter = userConverter;
? ? }
? ? @Override
? ? public UserDTO findById(Long id) {
? ? ? ? return null;
? ? }
}
正如我們所見(jiàn)劲阎,該方法目前返回 null绘盟。我們將在運(yùn)行測(cè)試后實(shí)現(xiàn)該功能。
第五步:我們創(chuàng)建測(cè)試類(lèi)來(lái)測(cè)試服務(wù)的findById方法
當(dāng)輸入中提供有效值時(shí)悯仙,甚至當(dāng)提供無(wú)效值時(shí)龄毡,良好的測(cè)試應(yīng)該驗(yàn)證方法的行為。
該服務(wù)依賴(lài)于存儲(chǔ)庫(kù)和轉(zhuǎn)換器锡垄。我們對(duì)存儲(chǔ)庫(kù)的實(shí)現(xiàn)不感興趣稚虎,因此模擬它是一個(gè)好主意。對(duì)于轉(zhuǎn)換器偎捎,我們可以考慮做同樣的事情,但由于它是一個(gè)非承蛉粒瑣碎的類(lèi)茴她,我們可以考慮在測(cè)試中使用真正的類(lèi),但不帶出Spring的上下文程奠。
建立一個(gè)測(cè)試類(lèi)
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
? ? @Mock
? ? private UserRepository userRepository;
? ? @Spy
? ? private UserConverter userConverter;
? ? private UserService userService;
? ? @BeforeEach
? ? public void init() {
? ? ? ? userService = new UserServiceImpl(userRepository, userConverter);
? ? }
? ? @Test
? ? public void findByIdSuccess() {
? ? ? ? User user = new User("Vincenzo", "Racca", "via Roma");
? ? ? ? user.setId(1L);
? ? ? ? when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));
? ? ? ? UserDTO userDTO = userService.findById(1L);
? ? ? ? verify(userRepository, times(1)).findById(anyLong());
? ? ? ? assertNotNull(userDTO);
? ? ? ? String[] surnameAndName = userDTO.getName().split( " ");
? ? ? ? assertEquals(2, surnameAndName.length);
? ? ? ? assertEquals(user.getSurname(), surnameAndName[0]);
? ? ? ? assertEquals(user.getName(), surnameAndName[1]);
? ? ? ? assertEquals(user.getAddress(), userDTO.getAddress());
? ? }
}
我們來(lái)分析一下代碼:
@ExtendWith(MockitoExtension.class) 允許我們使用 Mockito 庫(kù)的 mockato 上下文丈牢。 @ExtendWith 也對(duì)應(yīng)于 JUnit 4 的 @RunWith。
我們使用 @Mock 注釋存儲(chǔ)庫(kù)瞄沙,因?yàn)槲覀兿M?Mockito 創(chuàng)建接口的 mockata 實(shí)現(xiàn)己沛。
我們使用@Spy 對(duì)轉(zhuǎn)換器進(jìn)行注釋?zhuān)韵?Mockito 表明我們想要使用真正的類(lèi)。
我們使用 @BeforeEach 注釋每次運(yùn)行測(cè)試方法時(shí)初始化 userService 的 init 方法距境。由于我們使用了構(gòu)造函數(shù)依賴(lài)注入而不是字段注入申尼,我們只需要在構(gòu)造函數(shù)中傳入 mockato 存儲(chǔ)庫(kù)即可創(chuàng)建服務(wù)。 @BeforeEach 對(duì)應(yīng)于 JUnit 4 的 @Before 注解垫桂。
當(dāng)然這里也可用junit5或者是testng
我們來(lái)分析一下測(cè)試:
我們期望對(duì)于任何輸入 id师幕,存儲(chǔ)庫(kù)將返回一個(gè)名為 Vincenzo、姓氏 Racca 和通過(guò) Roma 的地址的用戶(hù)诬滩。為此霹粥,我們將 Mockito 的靜態(tài)方法與 anyLong(指示任何 id)一起使用,然后使用 thenReturn 指示我們期望從該方法中獲得的返回值疼鸟。當(dāng)我們調(diào)用該服務(wù)時(shí)后控,我們期望返回一個(gè)名為 Racca Vincenzo 和地址 via Roma.\ 的 UserDTO。通過(guò)驗(yàn)證空镜,我們驗(yàn)證服務(wù)調(diào)用存儲(chǔ)庫(kù)的 findById 1 次浩淘。然后遵循各種瑣碎的斷言捌朴。
我們執(zhí)行測(cè)試:它在調(diào)用 verify 時(shí)已經(jīng)失敗,因?yàn)榉?wù)方法從未調(diào)用存儲(chǔ)庫(kù)馋袜;
第六步:讓我們修復(fù)方法
public class UserNotFoundException extends RuntimeException {
? ? public UserNotFoundException(Long id) {
? ? ? ? super("User with id " + id + " not found!");
? ? }
}
@Test
public void findByIdUnSuccess() {
? ? when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
? ? UserNotFoundException exp = assertThrows(UserNotFoundException.class, () -> userService.findById(1L));
? ? assertEquals("User with id 1 not found!", exp.getMessage());
}
我們非常簡(jiǎn)單地告訴 Mockito男旗,對(duì)于任何 id,存儲(chǔ)庫(kù)都不會(huì)找到任何用戶(hù)欣鳖。使用 assertThrows 我們返回我們期望由服務(wù)啟動(dòng)的異常察皇,然后我們?cè)诋惓O⑸蠈?xiě)一個(gè)斷言。這種測(cè)試在 JUnit 5 中是可能的泽台。實(shí)際上什荣,在 JUnit 4 中,我們可以驗(yàn)證該方法是否啟動(dòng)了異常怀酷,但是一旦啟動(dòng)了錯(cuò)誤稻爬,您就無(wú)法繼續(xù)進(jìn)行斷言。
顯然測(cè)試失敗蜕依,所以讓我們修復(fù)方法桅锄。
現(xiàn)在測(cè)試通過(guò)了,我們可以評(píng)估以改進(jìn)代碼而不會(huì)忘記重新測(cè)試該方法样眠。
第七步:重構(gòu)方法
@Override
public UserDTO findById(Long id) {
? ? User user = userRepository.findById(id).orElseThrow(() -> {
? ? ? ? UserNotFoundException exp = new UserNotFoundException(id);
? ? ? ? LOGGER.error("Exception is UserServiceImpl.findById", exp);
? ? ? ? return exp;
? ? });
? ? return userConverter.userToUserDTO(user);
}
結(jié)論
我們通過(guò)使用 JUnit 5 開(kāi)發(fā)單元測(cè)試并在 Mockito 的幫助下模擬測(cè)試不感興趣的單元友瘤,簡(jiǎn)要了解了測(cè)試驅(qū)動(dòng)開(kāi)發(fā)的工作原理。在下一篇文章中檐束,我們將通過(guò)創(chuàng)建控制器來(lái)繼續(xù)開(kāi)發(fā)需求辫秧,我們將使用 Spring Boot、JUnit 5 和 H2 作為內(nèi)存數(shù)據(jù)庫(kù)編寫(xiě)集成測(cè)試被丧。