遇到mock 測試簡直就是神器,特别是要做代碼覆寫率,直接測試controller就好了,缺點,雖然可以復原事務,但是依賴資料庫資料,解決,根據SpringBoot ,再建立一個專門跑單元測試的資料庫,以及application.yml
想起以前用的 unitils 整合測試,巨額時間成本,都是在寫XML.遇到時間變化的條件,還一點辦法都沒有,唯一覺得是優勢的就是與環境解耦,不依賴資料庫
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnL4ADN1EjN5EzNtkTOxgDMyMjMyITMwEzNxAjMtUTNycTN58CXwEzNxAjMvwVN1IzN1kzLcd2bsJ2Lc12bj5ycn9Gbi52YucTMwIzcldWYtl2Lc9CX6MHc0RHaiojIsJye.png)
<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")){