编写干净的测试–验证或不验证
在編寫使用模擬對象的單元測試時,請遵循以下步驟:
第三步的描述實際上有點誤導,因為通常我們最終會驗證是否調用了正確的方法以及未調用模擬對象的其他方法。
每個人都知道,如果我們要編寫無錯誤的軟件,我們必須驗證這兩種情況或不良情況的發生。
對?
讓我們驗證一切
讓我們首先來看一下用于向數據庫添加新用戶帳戶的服務方法的實現。
此服務方法的要求是:
- 如果注冊用戶帳戶的電子郵件地址不是唯一的,我們的服務方法必須拋出異常。
- 如果注冊的用戶帳戶具有唯一的電子郵件地址,則我們的服務方法必須將新的用戶帳戶添加到數據庫中。
- 如果注冊的用戶帳戶具有唯一的電子郵件地址,并且是使用常規登錄創建的,則我們的服務方法必須先對用戶密碼進行編碼,然后再將其保存到數據庫中。
- 如果注冊的用戶帳戶具有唯一的電子郵件地址,并且是使用社交登錄創建的,則我們的服務方法必須保存使用的社交登錄提供商。
- 通過使用社交登錄創建的用戶帳戶必須沒有密碼。
- 我們的服務方法必須返回創建的用戶帳戶的信息。
如果要了解如何指定服務方法的要求,則應閱讀以下博客文章:
- 從上到下:Web應用程序的TDD
- 從構思到代碼:敏捷規范的生命周期
通過執行以下步驟來實現此服務方法:
RepositoryUserService類的源代碼如下所示:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;@Service public class RepositoryUserService implements UserService {private PasswordEncoder passwordEncoder;private UserRepository repository;@Autowiredpublic RepositoryUserService(PasswordEncoder passwordEncoder, UserRepository repository) {this.passwordEncoder = passwordEncoder;this.repository = repository;}@Transactional@Overridepublic User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException {if (emailExist(userAccountData.getEmail())) {throw new DuplicateEmailException("The email address: " + userAccountData.getEmail() + " is already in use.");}String encodedPassword = encodePassword(userAccountData);User registered = User.getBuilder().email(userAccountData.getEmail()).firstName(userAccountData.getFirstName()).lastName(userAccountData.getLastName()).password(encodedPassword).signInProvider(userAccountData.getSignInProvider()).build();return repository.save(registered);}private boolean emailExist(String email) {User user = repository.findByEmail(email);if (user != null) {return true;}return false;}private String encodePassword(RegistrationForm dto) {String encodedPassword = null;if (dto.isNormalRegistration()) {encodedPassword = passwordEncoder.encode(dto.getPassword());}return encodedPassword;} }如果我們要編寫單元測試以確保當用戶通過使用社交登錄注冊新用戶帳戶時我們的服務方法能夠正常工作,并且我們要驗證我們的服務方法與模擬對象之間的每一次交互,我們必須編寫八個對其進行單元測試。
我們必須確保:
- 當提供重復的電子郵件地址時,服務方法將檢查電子郵件地址是否唯一。
- 給定重復的電子郵件地址時,將引發DuplicateEmailException 。
- 給定重復的電子郵件地址時,service方法不會將新帳戶保存到數據庫中。
- 如果提供重復的電子郵件地址,我們的服務方法不會對用戶的密碼進行編碼。
- 當提供唯一的電子郵件地址時,我們的服務方法會檢查電子郵件地址是否唯一。
- 當給出唯一的電子郵件地址時,我們的服務方法將創建一個包含正確信息的新User對象,并將創建的User對象的信息保存到數據庫中。
- 當給出唯一的電子郵件地址時,我們的服務方法將返回創建的用戶帳戶的信息。
- 當指定唯一的電子郵件地址并使用社交登錄名時,我們的服務方法不得設置創建的用戶帳戶的密碼(或對其進行編碼)。
我們的測試類的源代碼如下所示:
import net.petrikainulainen.spring.social.signinmvc.user.dto.RegistrationForm; import net.petrikainulainen.spring.social.signinmvc.user.dto.RegistrationFormBuilder; import net.petrikainulainen.spring.social.signinmvc.user.model.SocialMediaService; import net.petrikainulainen.spring.social.signinmvc.user.model.User; import net.petrikainulainen.spring.social.signinmvc.user.repository.UserRepository; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.springframework.security.crypto.password.PasswordEncoder;import static com.googlecode.catchexception.CatchException.catchException; import static com.googlecode.catchexception.CatchException.caughtException; import static net.petrikainulainen.spring.social.signinmvc.user.model.UserAssert.assertThatUser; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class) public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldCheckThatEmailIsUnique() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());catchException(registrationService).registerNewUserAccount(registration);verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);}@Testpublic void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldThrowException() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());catchException(registrationService).registerNewUserAccount(registration);assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);}@Testpublic void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());catchException(registrationService).registerNewUserAccount(registration);verify(repository, never()).save(isA(User.class));}@Testpublic void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());catchException(registrationService).registerNewUserAccount(registration);verifyZeroInteractions(passwordEncoder);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCheckThatEmailIsUnique() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);registrationService.registerNewUserAccount(registration);verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);registrationService.registerNewUserAccount(registration);ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);verify(repository, times(1)).save(userAccountArgument.capture());User createdUserAccount = userAccountArgument.getValue();assertThatUser(createdUserAccount).hasEmail(REGISTRATION_EMAIL_ADDRESS).hasFirstName(REGISTRATION_FIRST_NAME).hasLastName(REGISTRATION_LAST_NAME).isRegisteredUser().isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertThatUser(createdUserAccount).hasEmail(REGISTRATION_EMAIL_ADDRESS).hasFirstName(REGISTRATION_FIRST_NAME).hasLastName(REGISTRATION_LAST_NAME).isRegisteredUser().isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);}@Testpublic void registerNewUserAccount_SocialSignInAnUniqueEmail_ShouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);registrationService.registerNewUserAccount(registration);verifyZeroInteractions(passwordEncoder);} }這些單元測試是按照本教程前面部分中給出的說明編寫的。
該課程有很多單元測試。 我們確定他們每個人都是真的必要嗎?
或者可能不是
一個明顯的問題是,我們編寫了兩個單元測試,兩個單元測試都驗證我們的服務方法檢查了用戶提供的電子郵件地址是否唯一。 我們可以通過將這些測試合并為一個單元測試來解決此問題。 畢竟,一項測試應該使我們相信,我們的服務方法會在創建新用戶帳戶之前驗證用戶提供的電子郵件地址是否唯一。
但是,如果這樣做,我們將找不到更有趣的問題的答案。 這個問題是:
我們是否應該真的驗證測試代碼和模擬對象之間的每一次交互?
幾個月前,我碰到了James Coplien撰寫的標題為: 為什么大多數單元測試都是浪費的文章。 本文提出了幾點要點,但其中之一非常適合這種情況。 詹姆斯·科普林(James Coplien)認為,對于測試套件中的每個測試,我們應該提出一個問題:
如果該測試失敗,那么將損害哪些業務要求?
他還解釋了為什么這是一個如此重要的問題:
在大多數情況下,答案是“我不知道”。 如果您不知道測試的價值,那么從理論上講,測試的商業價值可能為零。 測試確實要付出代價:維護,計算時間,管理等。 這意味著測試可能具有凈負值。 這是要刪除的第四類測試。
讓我們找出使用此問題評估單元測試時會發生什么。
彈出問題
當問一個問題時:“如果該測試失敗,將危及到哪些業務需求?” 關于測試類的每個單元測試,我們得到以下答案:
- 當提供重復的電子郵件地址時,服務方法將檢查電子郵件地址是否唯一。
- 用戶必須具有唯一的電子郵件地址。
- 給定重復的電子郵件地址時,將引發DuplicateEmailException 。
- 用戶必須具有唯一的電子郵件地址。
- 給定重復的電子郵件地址時,service方法不會將新帳戶保存到數據庫中。
- 用戶必須具有唯一的電子郵件地址。
- 如果提供重復的電子郵件地址,我們的服務方法不會對用戶的密碼進行編碼。
- –
- 當提供唯一的電子郵件地址時,我們的服務方法會檢查電子郵件地址是否唯一。
- 用戶必須具有唯一的電子郵件地址。
- 給定唯一的電子郵件地址后,我們的服務方法將創建一個包含正確信息的新User對象,并將創建的User對象的信息保存到使用的數據庫中。
- 如果注冊的用戶帳戶具有唯一的電子郵件地址,則必須將其保存到數據庫中。
- 當給出唯一的電子郵件地址時,我們的服務方法將返回創建的用戶帳戶的信息。
- 我們的服務方法必須返回創建的用戶帳戶的信息。
- 當指定唯一的電子郵件地址并使用社交登錄名時,我們的服務方法不得設置創建的用戶帳戶的密碼(或對其進行編碼)。
- 使用社交登錄創建的用戶帳戶沒有密碼。
乍一看,我們的測試類似乎只有一個沒有業務價值(或可能有負凈值)的單元測試。 此單元測試可確保當用戶嘗試使用重復的電子郵件地址創建新的用戶帳戶時,我們的代碼與PasswordEncoder模擬之間沒有任何交互。
很明顯,我們必須刪除此單元測試,但這不是唯一必須刪除的單元測試。
兔子洞比預期的深
早些時候我們注意到我們的測試類包含兩個單元測試,兩個單元測試都驗證是否調用了UserRepository接口的findByEmail()方法。 當我們仔細查看測試的服務方法的實現時,我們注意到:
- 當UserRepository接口的findByEmail()方法返回User對象時,我們的服務方法將引發DuplicateEmailException 。
- 當UserRepository接口的findByEmail()方法返回null時,我們的服務方法將創建一個新的用戶帳戶。
經過測試的服務方法的相關部分如下所示:
public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException {if (emailExist(userAccountData.getEmail())) {//If the PersonRepository returns a Person object, an exception is thrown.throw new DuplicateEmailException("The email address: " + userAccountData.getEmail() + " is already in use.");}//If the PersonRepository returns null, the execution of this method continues. }private boolean emailExist(String email) {User user = repository.findByEmail(email);if (user != null) {return true;}return false; }我認為我們應該刪除這兩個單元測試,原因有二:
- 只要我們正確配置了PersonRepository模擬,我們就知道它的findByEmail()方法是通過使用正確的方法參數調用的。 盡管我們可以將這些測試用例鏈接到業務需求(用戶的電子郵件地址必須是唯一的),但是我們不需要它們來驗證此業務需求沒有受到損害。
- 這些單元測試未記錄我們服務方法的API。 他們記錄了它的實現。 像這樣的測試是有害的,因為它們使我們的測試套件變得無關緊要,并且使重構更加困難。
如果我們不配置模擬對象,它們將返回“ nice”值。
Mockito常見問題解答指出:
為了透明和不干擾,默認情況下,所有Mockito模擬都返回“ nice”值。 例如:零,假,空集合或空。 請參閱有關存根的javadocs,以了解確切地返回了默認值。
這就是為什么我們應該始終配置相關的模擬對象的原因! 如果我們不這樣做,我們的測試可能就沒有用了。
讓我們繼續清理這個爛攤子。
清理混亂
從測試類中刪除這些單元測試后,其源代碼如下所示:
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.springframework.security.crypto.password.PasswordEncoder;import static com.googlecode.catchexception.CatchException.catchException; import static com.googlecode.catchexception.CatchException.caughtException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class) public class RepositoryUserServiceTest {private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";private static final String REGISTRATION_FIRST_NAME = "John";private static final String REGISTRATION_LAST_NAME = "Smith";private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldThrowException() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());catchException(registrationService).registerNewUserAccount(registration);assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);}@Testpublic void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());catchException(registrationService).registerNewUserAccount(registration);verify(repository, never()).save(isA(User.class));}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);registrationService.registerNewUserAccount(registration);ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);verify(repository, times(1)).save(userAccountArgument.capture());User createdUserAccount = userAccountArgument.getValue();assertThatUser(createdUserAccount).hasEmail(REGISTRATION_EMAIL_ADDRESS).hasFirstName(REGISTRATION_FIRST_NAME).hasLastName(REGISTRATION_LAST_NAME).isRegisteredUser().isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {@Overridepublic User answer(InvocationOnMock invocation) throws Throwable {Object[] arguments = invocation.getArguments();return (User) arguments[0];}});User createdUserAccount = registrationService.registerNewUserAccount(registration);assertThatUser(createdUserAccount).hasEmail(REGISTRATION_EMAIL_ADDRESS).hasFirstName(REGISTRATION_FIRST_NAME).hasLastName(REGISTRATION_LAST_NAME).isRegisteredUser().isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);}@Testpublic void registerNewUserAccount_SocialSignInAnUniqueEmail_ShouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {RegistrationForm registration = new RegistrationFormBuilder().email(REGISTRATION_EMAIL_ADDRESS).firstName(REGISTRATION_FIRST_NAME).lastName(REGISTRATION_LAST_NAME).isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER).build();when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);registrationService.registerNewUserAccount(registration);verifyZeroInteractions(passwordEncoder);} }我們從測試班級中刪除了三個單元測試,因此,我們可以享受以下好處:
- 我們的測試班的單元測試較少 。 這似乎是一個奇怪的好處,因為通常建議我們編寫盡可能多的單元測試。 但是,如果考慮到這一點,則減少單元測試是有意義的,因為我們需要維護的測試較少。 這以及每個單元只能測試一件事的事實使我們的代碼更易于維護和重構。
- 我們已經提高了文檔的質量 。 刪除的單元測試未記錄測試服務方法的公共API。 他們記錄了它的實施。 由于這些測試已刪除,因此更容易弄清測試服務方法的要求。
摘要
這篇博客文章教會了我們三件事:
- 如果我們無法確定在單元測試失敗的情況下受到損害的業務需求,則不應編寫該測試。
- 我們不應該編寫沒有記錄測試方法的公共API的單元測試,因為這些測試使我們的代碼(和測試)更加難以維護和重構。
- 如果發現現有的單元測試違反了這兩個規則,則應將其刪除。
在本教程中,我們取得了很多成就。 您認為可以使這些單元測試變得更好嗎?
如果您想了解有關編寫干凈測試的更多信息,請閱讀我的編寫干凈測試教程的所有部分 。
翻譯自: https://www.javacodegeeks.com/2014/08/writing-clean-tests-to-verify-or-not-to-verify.html
總結
以上是生活随笔為你收集整理的编写干净的测试–验证或不验证的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaFX技巧11:更新只读属性
- 下一篇: 安卓c编译器(安卓c编译)