遇到mock 測試簡直就是神器,特別是要做代碼覆蓋率,直接測試controller就好了,缺點,雖然可以回滾事務,但是依賴數據庫數據,解決,根據SpringBoot ,再建立一個專門跑單元測試的數據庫,以及application.yml
想起以前用的 unitils 整合測試,巨額時間成本,都是在寫XML.遇到時間變化的條件,還一點辦法都沒有,唯一覺得是優勢的就是與環境解耦,不依賴數據庫



pom配置
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <!--<configuration>--> <!--<skip>true</skip>--> <!--</configuration>--> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.6.1</version> <configuration> <!--<skip>true</skip>--> <source>1.8</source> <target>1.8</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <configuration> <includes>com.*</includes> </configuration> <executions> <execution> <id>pre-test</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>post-test</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin>
jenkins 集成了 sonar 配置 Analysis properties 一定要配置 sonar.tests=src/test/java 否則會吧單元測試的覆蓋率也算上,坑爹,默認就應該排除掉
sonar.exclusions 可以排除掉,自相矛盾的規則(永遠達不成,例如 全是靜態方法的類,可以寫成final 並且添加私有構造方法,這個私有構造方法,永遠都覆蓋不了,除非你還去反射調用),或者測試不會執行的類,這里我排除了SpringBoot的啟動類
sonar.projectKey=testKey
sonar.projectName=testProject
sonar.projectVersion=1.0
sonar.sources=src/main/java
sonar.tests=src/test/java
sonar.exclusions=src/main/java/com/test/Application.java
sonar.java.binaries=target/classes
sonar.language=java
sonar.sourceEncoding=UTF-8
回滾事務
在測試類加上注解@Rollback
注意:之前喜歡在Controller 使用 @Transactional(propagation = Propagation.REQUIRES_NEW),但是這樣的話,是強行開啟一個新事務,不會加入上層事務,所以哪怕是Controller 也應該使用@Transactional(propagation = Propagation.REQUIRED)
否則@Rollback無效
如果是微服務SpringCloud,回滾不了另一個服務的事務,那么直接進入FallBack服務降級就好了
以下是我的測試類的注解部分
@RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Rollback @Transactional @ActiveProfiles(profiles = "dev") public class BaseTests { }
加入SpringSecurity過濾器鏈
模擬用戶登錄,注意登錄用戶的權限問題
@Autowired protected TestRestTemplate restTemplate; @Autowired protected WebApplicationContext wac; @Autowired private Filter springSecurityFilterChain; @Autowired private Filter invoiceContextFilter; protected MockMvc mockMvc; protected MockHttpSession session; @PostConstruct public void setup() throws Exception { this.mockMvc = MockMvcBuilders.webAppContextSetup(wac) .addFilters(springSecurityFilterChain, invoiceContextFilter) .build(); this.session = new MockHttpSession(); login(); getLoginSession("testuser", "789456a"); } protected void login() throws Exception { MvcResult result = this.mockMvc .perform(get("/login")) .andReturn(); Assert.assertNotNull(result.getModelAndView()); } /** * 獲取登入信息session * * @return * @throws Exception */ protected void getLoginSession(String name, String pwd) throws Exception { MvcResult result = this.mockMvc .perform(post("/doLogin").contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("username", name).param("password",pwd) .param("verifiCode", "ABCD")) .andExpect(status().isFound()) .andReturn(); MockHttpSession mockHttpSession = (MockHttpSession) result.getRequest().getSession(); this.mockMvc .perform(get("/success").session(mockHttpSession)) .andExpect(status().isOk()); this.session = mockHttpSession; } }
作用域為session 的bean 處理
controller 依賴注入了 作用域為 @Scope(scopeName = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) 的bean
例如
@Component("cuzSessionAttributes")
@Scope(scopeName = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserAttributes implements Serializable {
}
JunitTest中,可以從MockHttpSession取出這個bean,然后修改bean的屬性,再設置回session,以達到測試分支覆蓋率的作用
@Test public void testDo() throws Exception { UserAttributes userAttributes = (UserAttributes )session.getAttribute("scopedTarget.userAttributes"); userAttributes.setSomeThing("abc"); session.setAttribute("scopedTarget.userAttributes",userAttributes); this.mockMvc .perform(get("/do") .session(session)).andExpect(status().isOk()); }
上傳文件測試
@Test public void upload() throws Exception { InputStream inputStream = this.getClass().getResourceAsStream("/test.xlsx"); MockMultipartFile multipartFile = new MockMultipartFile("txt_file","test.xlsx","multipart/form-data; boundary=----WebKitFormBoundarybF0B6B6hk52YSBvk", inputStream); MvcResult result = this.mockMvc.perform( fileUpload("/upload").file(multipartFile).session(session) ) .andExpect(status().isOk()).andReturn(); }
uploadController
網上流傳着一段上傳多文件的代碼,這段代碼mock上傳是會轉換異常的
@RequestMapping(value = "/batchUpload", method = RequestMethod.POST) public Result String batchUpload(HttpServletRequest request) { List<MultipartFile> files = ((MultipartHttpServletRequest) request).getFiles("file"); //***** } }
改為這樣
/** * 上傳 */ @PostMapping(path = "/upload", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @Transactional(propagation = Propagation.REQUIRED) public Result upload(MultipartHttpServletRequest multiRequest) { Iterator<String> iter = multiRequest.getFileNames(); while (iter.hasNext()) { MultipartFile file = multiRequest.getFile(iter.next()); //***** }
文件下載測試
@Test public void download() throws Exception { MvcResult mvcResult = this.mockMvc .perform(get("/download") .accept(MediaType.APPLICATION_OCTET_STREAM) .session(session)) .andExpect(status().isOk()).andReturn(); Assert.assertNotNull(mvcResult.getResponse().getContentAsByteArray()); }
驗證碼問題
SpringBoot解決方式 Environment
獲取環境,這里根據junit環境還有dev環境判斷,可以跳過驗證碼驗證
@Autowired private Environment env; String active = env.getProperty("spring.profiles.active"); if(!active.equals("junit")&&!active.equals("dev")){
