具有营销型网站的公司,精美旅游网站案例,做内网网站教程,安徽建筑培训网为鼓励单元测试#xff0c;特分门别类示例各种组件的测试代码并进行解说#xff0c;供开发人员参考。
本文中的测试均基于JUnit5。
单元测试实战#xff08;一#xff09;Controller 的测试
单元测试实战#xff08;二#xff09;Service 的测试
单元测试实战特分门别类示例各种组件的测试代码并进行解说供开发人员参考。
本文中的测试均基于JUnit5。
单元测试实战一Controller 的测试
单元测试实战二Service 的测试
单元测试实战三JPA 的测试
单元测试实战四MyBatis-Plus 的测试
单元测试实战五普通类的测试
单元测试实战六其它
概述
Controller的测试要点在于模拟一个HTTP请求过来相应的handler方法能正确处理之。
测试应遵循经典三段式given、when、then即假设xxx……那么当yyy时……应该会zzz。
测试类推荐使用WebMvcTest注解并传入要测试的Controller类作为参数。
在每个测试之前应清理/重置测试数据即模拟前端发过来的请求参数或请求体。
断言应主要检查响应对象包括返回码。
依赖
测试使用JUnit以及Spring Boot自带的测试工具集如Mockito、Hamcrest、Assertj等后续章节也一样。依赖如下
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope
/dependency
dependencygroupIdorg.junit.jupiter/groupIdartifactIdjunit-jupiter-api/artifactIdscopetest/scope
/dependency
示例1
以下是一个控制器UserController主要完成对User实体的CRUD功能
package com.aaa.api.auth.controller;import com.aaa.api.auth.entity.User;
import com.aaa.api.auth.service.UserService;
import com.aaa.sdk.rest.global.EnableGlobalResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;import java.util.List;RestController
RequestMapping(/users)
EnableGlobalResult
public class UserController {private final Logger log LoggerFactory.getLogger(UserController.class);private final UserService service;public UserController(UserService service) {this.service service;}GetMapping(/{id})public User getUser(PathVariable(id) Long id) {return service.findById(id);}GetMapping(/q)public User findUser(RequestParam(userCode) String userCode) {return service.findByUserCode(userCode);}GetMappingpublic ListUser getAll() {return service.findAll();}PostMappingpublic User save(RequestBody User user) {return service.save(user);}
}
以下是对UserController进行测试的测试类
package com.aaa.api.auth.controller;import com.aaa.api.auth.entity.User;
import com.aaa.api.auth.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;import java.util.List;import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;WebMvcTest(UserController.class)
class UserControllerTest {Autowiredprivate MockMvc mockMvc;MockBeanprivate UserService svc;Autowiredprivate ObjectMapper objectMapper;private final User u1 new User();private final User u2 new User();private final User u3 new User();BeforeEachvoid setUp() {u1.setName(张三);u1.setUserCode(zhangsan);u1.setRole(User.ADMIN);u1.setEmail(zhangsanaaa.net.cn);u1.setMobile(13600001234);u2.setName(李四);u2.setUserCode(lisi);u2.setRole(User.ADMIN);u2.setEmail(lisiaaa.net.cn);u2.setMobile(13800001234);u3.setName(王五);u3.setUserCode(wangwu);u3.setRole(User.USER);u3.setEmail(wangwuaaa.net.cn);u3.setMobile(13900001234);}Testvoid testGetUser() throws Exception {// given - precondition or setuplong id 1L;given(svc.findById(id)).willReturn(u1);// when - action or the behaviour that we are going testResultActions response mockMvc.perform(get(/users/{id}, id));// then - verify the outputresponse.andDo(print()).andExpect(status().isOk()).andExpect(jsonPath($.userCode, is(u1.getUserCode()))).andExpect(jsonPath($.name, is(u1.getName()))).andExpect(jsonPath($.email, is(u1.getEmail())));}Testvoid testFindUser() throws Exception {// given - precondition or setupgiven(svc.findByUserCode(any())).willReturn(u1);// when - action or the behaviour that we are going testResultActions response mockMvc.perform(get(/users/q?userCodezhangsan));// then - verify the outputresponse.andDo(print()).andExpect(status().isOk()).andExpect(jsonPath($.userCode, is(u1.getUserCode())));}Testvoid testGetAll() throws Exception {// given - precondition or setupListUser listOfUsers List.of(u1, u2, u3);given(svc.findAll()).willReturn(listOfUsers);// when - action or the behaviour that we are going testResultActions response mockMvc.perform(get(/users));// then - verify the outputresponse.andDo(print()).andExpect(status().isOk()).andExpect(jsonPath($.size(), is(listOfUsers.size())));}Testvoid testSave() throws Exception {// given - precondition or setupgiven(svc.save(any())).willAnswer((invocation)- invocation.getArgument(0));// when - action or behaviour that we are going testResultActions response mockMvc.perform(post(/users).contentType(MediaType.APPLICATION_JSON).content(objectMapper.writeValueAsString(u1)));// then - verify the result or output using assert statementsresponse.andDo(print()).andExpect(status().isOk()) // its NOT created, as our controller is returning 200..andExpect(jsonPath($.userCode, is(u1.getUserCode()))).andExpect(jsonPath($.name, is(u1.getName()))).andExpect(jsonPath($.role, equalTo(u1.getRole().intValue())));}
}
测试类说明
第28行我们声明这是个WebMvcTest并且是对UserController类进行测试。
第32行我们声明了一个MockMvc对象并标注为AutoWired这是Spring提供的MVC测试组件通常每个Controller测试类都需要。
第35行我们声明了一个MockBean是个Service。因为UserController会注入一个UserService对象所以这里我们Mock了一个MockBean注解会将Mock出的对象加入测试application context。
第38行的ObjectMapper则是我们测试中用来进行对象-JSON转换的工具使用AutoWired是因为测试框架本身就提供这种Bean。
第40-42行提供了三个测试数据并在setUp()方法中进行初始化/重置。BeforeEach注解使得setUp()方法在每个测试之前都会执行一遍。
接下来从65行开始是测试方法每个方法都遵循given - when - then三段式。
testGetUser方法是测试根据id获取User对象的。它假设service组件的findById(1)会返回对象u1那么当访问/users/1这个路径时即调用UserController的getUser方法时返回的响应体就应该是转成JSON串的u1对象。注意我们使用jsonPath来取返回对象的属性$代表根对象。
testFindUser方法是测试根据用户编码查询User对象的。它假设service组件的findByUserCode()无论传什么参数都会返回对象u1那么当访问/users/q?userCodezhangsan这个路径时即调用UserController的findUser方法时返回的响应体就应该是转成JSON串的u1对象。
testGetAll方法是测试获取所有User对象的。它假设service组件的findAll()会返回对象u1、u2、u3那么当访问/users这个路径时即调用UserController的getAll方法时返回的响应体就应该是转成JSON串的u1、u2、u3对象集合。
testSave方法是测试保存User对象的。它假设service组件在save()任何User对象时都会返回该对象本身那么当对/users这个路径进行POST时即调用UserController的save方法时返回的响应体就应该是转成JSON串该User对象。
示例2
以下是集成SSO的Controller即实现code换token功能的redirect_uri端点以及logout端点SSOIntegrationController
package com.aaa.api.auth.controller;import com.aaa.sdk.auth.sso.OAuth2Token;
import com.aaa.sdk.auth.sso.OAuth2TokenHelper;
import com.aaa.sdk.rbac.RbacCacheNames;
import com.aaa.sdk.rest.global.GlobalResult;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.io.IOException;
import java.util.Map;/*** 与集团SSO的集成。提供code换token即所谓的redirect_uri端点、logout端点。*/
RestController
RequestMapping(/oauth)
public class SSOIntegrationController {private final Logger log LoggerFactory.getLogger(SSOIntegrationController.class);private final OAuth2TokenHelper oAuth2TokenHelper;private final CacheManager cacheManager;public SSOIntegrationController(OAuth2TokenHelper oAuth2TokenHelper, CacheManager cacheManager) {this.oAuth2TokenHelper oAuth2TokenHelper;this.cacheManager cacheManager;}/*** 此为用授权码换token的端点即redirect_uri。* param code 授权码* param state 获取token之后经过Base64编码的重定向url。* param request http请求* param response http响应* return 一个包含username和access_token两个键值对的Map。*/GetMapping({/token, callback})public GlobalResultMapString, String getTokenByCode(RequestParam(name code, required false) String code,RequestParam(name state, required false) String state,HttpServletRequest request,HttpServletResponse response) throws IOException {// 如果没有指定redirectUri则使用当前的URIString fallbackRedirectUri getFullURL(request);if (!StringUtils.hasText(code)) {// 如果授权码为空则先请求授权码String authorizeCodeUri oAuth2TokenHelper.buildAuthorizeCodeUri(state, fallbackRedirectUri);response.sendRedirect(authorizeCodeUri);return null;}// 使用授权码换取tokenOAuth2Token oAuth2Token oAuth2TokenHelper.getAccessToken(code, fallbackRedirectUri);if (oAuth2Token null) {log.error(Failed in exchanging token with code!);return GlobalResult.fail(101001, 获取token失败);}// 清除登录用户权限的缓存RbacCacheNames.evictUserCache(cacheManager, oAuth2Token.getUsername());// 重定向当前url到指定redirectUriString loginRedirectUri oAuth2TokenHelper.getLoginRedirectUri(state, oAuth2Token.getAccessToken());log.debug(Will redirect user to {}, loginRedirectUri);response.sendRedirect(loginRedirectUri);log.info(User {}/{} login, oAuth2Token.getUserId(), oAuth2Token.getUsername());return GlobalResult.succeed(Map.of(username, oAuth2Token.getUsername(),access_token, oAuth2Token.getAccessToken()));}/*** 用户登出。* param state 获取token之后经过Base64编码的重定向url* param request http请求* param response http响应*/GetMapping(/logout)public void logout(RequestParam(name state, required false) String state,HttpServletRequest request,HttpServletResponse response) throws IOException {String fallbackRedirectUri getFullURL(request).replaceAll(/logout, /token);String logoutUri oAuth2TokenHelper.buildLogoutUri(state, fallbackRedirectUri);response.sendRedirect(logoutUri);}private String getFullURL(HttpServletRequest request) {StringBuilder requestURL new StringBuilder(request.getRequestURL().toString());String queryString request.getQueryString();if (queryString null) {return requestURL.toString();} else {return requestURL.append(?).append(queryString).toString();}}}
以下是对SSOIntegrationController进行测试的测试类
package com.aaa.api.auth.controller;import com.aaa.sdk.auth.sso.OAuth2Token;
import com.aaa.sdk.auth.sso.OAuth2TokenHelper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;WebMvcTest(SSOIntegrationController.class)
class SSOIntegrationControllerTest {Autowiredprivate MockMvc mockMvc;MockBeanprivate OAuth2TokenHelper tokenHelper;private final OAuth2Token oauth2Token new OAuth2Token();{oauth2Token.setAccessToken(123);oauth2Token.setRefreshToken(456);oauth2Token.setJti(abc);oauth2Token.setTokenType(jwt);oauth2Token.setExpiresIn(3600 * 1000);oauth2Token.setUserId(zhangsan);oauth2Token.setUsername(zhangsan);}Testvoid testGetTokenByCode() throws Exception {// given - precondition or setupgiven(tokenHelper.getAccessToken(any(), any())).willReturn(oauth2Token);given(tokenHelper.getLoginRedirectUri(any(), any())).willReturn(https://foo/bar);// when - action or behaviour that we are going testResultActions response mockMvc.perform(get(/oauth/token?codemycodestatemystate));// then - verify the result or output using assert statementsresponse.andDo(print()).andExpect(status().isFound()).andExpect(header().string(Location, https://foo/bar)).andExpect(jsonPath($.code, is(0))).andExpect(jsonPath($.success, is(true))).andExpect(jsonPath($.data.username, is(zhangsan))).andExpect(jsonPath($.data.access_token, is(123)));}Testvoid testGetTokenByCode_EmptyCode() throws Exception {// given - precondition or setupsgiven(tokenHelper.buildAuthorizeCodeUri(any(), any())).willReturn(https://foo/oauth/token);// when - action or behaviour that we are going testResultActions response mockMvc.perform(get(/oauth/token?statemystate));// then - verify the result or output using assert statementsMvcResult result response.andDo(print()).andExpect(status().isFound()).andReturn();String content result.getResponse().getContentAsString();assertThat(content.length()).isEqualTo(0);}Testvoid testGetTokenByCode_NullAccessToken() throws Exception {// given - precondition or setupsgiven(tokenHelper.getAccessToken(any(), any())).willReturn(null);// when - action or behaviour that we are going testResultActions response mockMvc.perform(get(/oauth/token?codemycodestatemystate));// then - verify the result or output using assert statementsresponse.andDo(print()).andExpect(status().isOk()).andExpect(jsonPath($.success, is(false))).andExpect(jsonPath($.code, not(0)));}Testvoid testLogout() throws Exception {// given - precondition or setupsgiven(tokenHelper.buildLogoutUri(any(), any())).willReturn(https://foo/bar/login);// when - action or behaviour that we are going testResultActions response mockMvc.perform(get(/oauth/logout?statemystate));// then - verify the result or output using assert statementsresponse.andDo(print()).andExpect(status().isFound()).andExpect(header().string(Location, https://foo/bar/login));}
}
测试类说明
第23行我们声明这是个WebMvcTest并且是对SSOIntegrationController类进行测试。
第27行我们同样需要一个MockMvc组件不再赘述。
第30行我们有一个类型为OAuth2TokenHelper的MockBean这是因为SSOIntegrationController里有这个组件我们需要mock其行为。
第32行我们定义了一个OAuth2Token类型的测试数据oauth2Token并且在34行的初始化块中初始化其各个属性。这是个测试中会用到的JWT令牌对象。
接下来从第44行开始是测试方法每个方法都遵循given - when - then三段式。
testGetTokenByCode方法是测试code换token。它假设tokenHelper.getAccessToken会返回我们的测试数据oauth2Token且tokenHelper.getLoginRedirectUri会返回https://foo/bar那么当访问/oauth/token?codemycodestatemystate这个路径时即调用SSOIntegrationController的getTokenByCode方法时返回的响应体就应该是转成JSON串的oauth2Token对象且响应会指示浏览器重定向到https://foo/bar。该测试覆盖了getTokenByCode方法的正常分支。
testGetTokenByCode_EmptyCode方法同样是测试code换token。它假设tokenHelper.buildAuthorizeCodeUri会返回https://foo/oauth/token那么当访问/oauth/token?statemystate这个路径时即调用SSOIntegrationController的getTokenByCode方法但code为空时返回的响应体是空。该测试覆盖了SSOIntegrationController第55行的if分支。注意在这个方法里我们取了response内容来进行assert没有用jsonPath。
testGetTokenByCode_NullAccessToken方法仍旧测试code换token。它假设tokenHelper.getAccessToken会返回null那么当访问/oauth/token?codemycodestatemystate这个路径时即调用SSOIntegrationController的getTokenByCode方法时返回的响应体是一个不成功的GlobalResult。该测试覆盖了SSOIntegrationController第63行的if分支。
testLogout方法是测试登出功能。它假设tokenHelper.buildLogoutUri返回https://foo/bar/login那么当访问/oauth/logout?statemystate这个路径时即调用SSOIntegrationController的logout方法时返回的响应体会指示浏览器重定向到https://foo/bar/login。
总结
对于一个WebMvcTest我们一般都注入一个MockMvc组件。
对于待测类依赖的组件典型的如Service我们通常使用MockBean来模拟出一个。如果不是组件不是Spring Bean我们可以直接用Mock模拟。对于方便new出来的也可以直接new出来比如我们的三个user实体和oauth2Token对象。Mockito还有一个Spy注解可以监控被注解的对象该对象通常new出来但Spy也可以像Mock那样对其行为进行打桩。总之Mock、Spy、MockBean、SpyBean、Autowired以及new出来的对象都是为了模拟、订制该Controller所依赖的各种对象及其行为方便我们测试。这种模拟或曰打桩是为了让我们能专注于待测试类的行为而不致被其依赖的东西转移了注意力。假如我们都用真实的依赖比如UserService那么UserService自身的bug会导致我们的UserControllerTest失败进一步而言UserService又依赖于UserRepositoy如果这个UserRepository有bug那它又会同时导致UserServiceTest和UserControllerTest都失败——这就不是单元测试而是集成测试了单元测试通常只关注单一类。