编写干净的测试–提防魔术
很難為干凈的代碼找到一個好的定義,因為我們每個人都有自己的單詞clean的定義。 但是,有一個似乎是通用的定義:
干凈的代碼易于閱讀。
這可能會讓您感到有些驚訝,但是我認為該定義也適用于測試代碼。 使測試盡可能具有可讀性是我們的最大利益,因為:
- 如果我們的測試易于閱讀,那么很容易理解我們的代碼是如何工作的。
- 如果我們的測試易于閱讀,那么如果測試失敗(不使用調試器),很容易發現問題。
編寫干凈的測試并不難,但是需要大量的實踐,這就是為什么如此多的開發人員為此苦苦掙扎的原因。
我也為此感到掙扎,這就是為什么我決定與您分享我的發現的原因。
這是我教程的第三部分,描述了我們如何編寫干凈的測試。 這次,我們將學習兩種可用于從測試中刪除幻數的技術。
救援常量
我們使用在我們的代碼常量,因為沒有常量我們的代碼將與被散落幻數 。 使用幻數有兩個結果:
換一種說法,
- 常數幫助我們用描述其存在原因的某種事物來代替幻數。
- 常量使我們的代碼更易于維護,因為如果常量的值發生變化,我們只需將該更改僅保留到一個位置即可。
如果我們考慮從測試用例中找到的幻數,我們會注意到它們可以分為兩組:
讓我們仔細看看這兩種情況。
在測試類中聲明常量
那么,為什么我們要在測試類中聲明一些常量呢?
畢竟,如果我們考慮使用常量的好處,首先想到的是,我們應該通過創建包含測試中使用的常量的類來消除測試中的幻數。 例如,我們可以創建一個TodoConstants類,其中包含TodoControllerTest , TodoCrudServiceTest和TodoTest類中使用的常量。
這是一個壞主意 。
盡管有時候以這種方式共享數據是明智的,但我們不應輕易做出這個決定,因為在大多數情況下,我們在測試中引入常數的唯一動機是避免輸入錯誤和幻數。
另外,如果幻數僅與單個測試類相關,則將這種依賴關系引入我們的測試是沒有道理的,因為我們想最大程度地減少創建的常量的數量。
我認為,處理這種情況的最簡單方法是在測試類中聲明常量。
讓我們找出如何改進本教程前面部分中描述的單元測試。 編寫該單元測試以測試RepositoryUserService類的registerNewUserAccount()方法,并且當使用社交符號提供者和唯一的電子郵件地址創建新的用戶帳戶時,它驗證此方法是否正常工作。
該測試用例的源代碼如下所示:
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; 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 org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when;@RunWith(MockitoJUnitRunner.class) public class RepositoryUserServiceTest {private RepositoryUserService registrationService;@Mockprivate PasswordEncoder passwordEncoder;@Mockprivate UserRepository repository;@Beforepublic void setUp() {registrationService = new RepositoryUserService(passwordEncoder, repository);}@Testpublic void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationForm();registration.setEmail("john.smith@gmail.com");registration.setFirstName("John");registration.setLastName("Smith");registration.setSignInProvider(SocialMediaService.TWITTER);when(repository.findByEmail("john.smith@gmail.com")).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);assertEquals("john.smith@gmail.com", createdUserAccount.getEmail());assertEquals("John", createdUserAccount.getFirstName());assertEquals("Smith", createdUserAccount.getLastName());assertEquals(SocialMediaService.TWITTER, createdUserAccount.getSignInProvider());assertEquals(Role.ROLE_USER, createdUserAccount.getRole());assertNull(createdUserAccount.getPassword());verify(repository, times(1)).findByEmail("john.smith@gmail.com");verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);} }問題在于,此測試用例在創建新的RegistrationForm對象,配置UserRepository模擬的行為,驗證返回的User對象的信息是否正確以及驗證是否調用了UserRepository模擬的正確方法時使用了幻數。在經過測試的服務方法中。
通過在測試類中聲明常量來刪除這些幻數之后,測試的源代碼如下所示:
import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; 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 org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; 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 Role ROLE_REGISTERED_USER = Role.ROLE_USER;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_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {RegistrationForm registration = new RegistrationForm();registration.setEmail(REGISTRATION_EMAIL_ADDRESS);registration.setFirstName(REGISTRATION_FIRST_NAME);registration.setLastName(REGISTRATION_LAST_NAME);registration.setSignInProvider(SOCIAL_SIGN_IN_PROVIDER);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);assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());assertNull(createdUserAccount.getPassword());verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);verify(repository, times(1)).save(createdUserAccount);verifyNoMoreInteractions(repository);verifyZeroInteractions(passwordEncoder);} }此示例說明在測試類中聲明常量具有三個好處:
但是,有時我們的測試使用與多個測試類別真正相關的幻數。 讓我們找出如何應對這種情況。
將常量添加到非實例性類
如果該常量與多個測試類相關,則在使用該常量的每個測試類中聲明該常量是沒有意義的。 讓我們看一下一種情況,將常量添加到非實例化類是有意義的。
假設我們必須為REST API編寫兩個單元測試:
- 第一個單元測試確保我們不能向數據庫添加空的待辦事項。
- 第二個單元測試確保我們不能向數據庫添加空筆記。
這些單元測試使用Spring MVC測試框架。 如果您不熟悉它,則可能要看一看我的
Spring MVC測試教程 。
第一個單元測試的源代碼如下所示:
import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext;import java.nio.charset.Charset;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {WebUnitTestContext.class}) @WebAppConfiguration public class TodoControllerTest {private static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(),Charset.forName("utf8"));private MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate WebApplicationContext webAppContext;@Beforepublic void setUp() {mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();}@Testpublic void add_EmptyTodoEntry_ShouldReturnHttpRequestStatusBadRequest() throws Exception {TodoDTO addedTodoEntry = new TodoDTO();mockMvc.perform(post("/api/todo").contentType(APPLICATION_JSON_UTF8).content(objectMapper.writeValueAsBytes(addedTodoEntry))).andExpect(status().isBadRequest());} }第二個單元測試的源代碼如下所示:
import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext;import java.nio.charset.Charset;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {WebUnitTestContext.class}) @WebAppConfiguration public class NoteControllerTest {private static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(),Charset.forName("utf8"));private MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate WebApplicationContext webAppContext;@Beforepublic void setUp() {mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();}@Testpublic void add_EmptyNote_ShouldReturnHttpRequestStatusBadRequest() throws Exception {NoteDTO addedNote = new NoteDTO();mockMvc.perform(post("/api/note").contentType(APPLICATION_JSON_UTF8).content(objectMapper.writeValueAsBytes(addedNote))).andExpect(status().isBadRequest());} }這兩個測試類都聲明一個名為APPLICATION_JSON_UTF8的常量。 該常數指定請求的內容類型和字符集。 同樣,很明顯,在每個測試類中都需要此常量,其中包含用于控制器方法的測試。
這是否意味著我們應該在每個這樣的測試類中聲明此常量?
沒有!
由于以下兩個原因,我們應將此常量移至非實例化類:
讓我們創建一個最終的WebTestConstants類,將APPLICATION_JSON_UTF8常量移動到該類,然后向創建的類添加一個私有構造函數。
WebTestConstant類的源代碼如下所示:
import org.springframework.http.MediaType;public final class WebTestConstants {public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(),Charset.forName("utf8"));private WebTestConstants() {} }完成此操作后,我們可以從測試類中刪除APPLICATION_JSON_UTF8常量。 我們的新測試的源代碼如下所示:
import com.fasterxml.jackson.databind.ObjectMapper; import net.petrikainulainen.spring.jooq.config.WebUnitTestContext; import net.petrikainulainen.spring.jooq.todo.dto.TodoDTO; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext;import java.nio.charset.Charset;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {WebUnitTestContext.class}) @WebAppConfiguration public class TodoControllerTest {private MockMvc mockMvc;@Autowiredprivate ObjectMapper objectMapper;@Autowiredprivate WebApplicationContext webAppContext;@Beforepublic void setUp() {mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();}@Testpublic void add_EmptyTodoEntry_ShouldReturnHttpRequestStatusBadRequest() throws Exception {TodoDTO addedTodoEntry = new TodoDTO();mockMvc.perform(post("/api/todo").contentType(WebTestConstants.APPLICATION_JSON_UTF8).content(objectMapper.writeValueAsBytes(addedTodoEntry))).andExpect(status().isBadRequest());} }我們剛剛從測試類中刪除了重復的代碼,并減少了為控制器編寫新測試所需的工作。 太酷了吧?
如果我們更改添加到常量類的常量的值,則此更改將影響使用該常量的每個測試用例。 這就是為什么我們應該最小化添加到常量類的常量的數量 。
摘要
現在我們知道,常數可以幫助我們編寫干凈的測試,并減少編寫新測試和維護現有測試所需的工作量。 將本博客文章中給出的建議付諸實踐時,我們需要記住以下幾點:
- 我們必須給常量和常量類起好名字 。 如果我們不這樣做,就不會利用這些技術的全部潛力。
- 在不弄清楚我們想要用該常數實現什么的情況下,我們不應該引入新的常數。 實際情況通常比此博客文章的示例復雜得多。 如果我們在自動駕駛儀上編寫代碼,很可能會錯過針對當前問題的最佳解決方案。
翻譯自: https://www.javacodegeeks.com/2014/05/writing-clean-tests-beware-of-magic.html
總結
以上是生活随笔為你收集整理的编写干净的测试–提防魔术的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓英雄无敌3(安卓英雄无敌)
- 下一篇: 安卓乐动力查看id(安卓乐动力)