文章目录
代码在 https://github.com/betterGa/SpringBootDemo
一、Spring Boot 单元测试
单元测试是开发人员为确保单个单元或组件功能正常工作而进行的测试之一。
1、controller层的单元测试
主要借助于 spring-test 包的 MockMvc 来进行模拟浏览器访问的,也就是说, MockMvc 是 Spring 框架里的东西,这在之前演示过一个查询的案例:
接下来把 增 也进行测试。
💎 代码如下:
增 控制类:
@PostMapping(value="/user")
public List<Person> createUser(@RequestBody Person person){
person.setId("4");
list.add(person);
return list;
}
可以看到,需要传入的参数是 Person 实体类的 Json 形式,测试类:
// 增
@Test
public void addUser() throws Exception {
Person person = new Person("5", "jia5");
ObjectMapper objectMapper = new ObjectMapper();
String jsonStr = objectMapper.writeValueAsString(person);
String ret = mockMvc.perform(MockMvcRequestBuilders
.post("/user")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonStr))
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print())
.andReturn().getResponse().getContentAsString();
System.out.println("ret=======" + ret);
}
运行结果,状态值 200,没问题的:
2、service层的单元测试
service 层的单元测试我们将引入 mockito 测试框架, Mockito 是 mocking 框架,它让你用简洁的 API 做测试。而且 Mockito 简单易学,它可读性强和验证语法简洁。
Mockito 是一个针对 Java 的单元测试模拟框架,它与 EasyMock 和 jMock 很相似,都是为了简化单元测试过程中测试上下文 ( 或者称之为测试驱动函数以及桩函数 ) 的搭建而开发的工具。
要将Mockito Mocks注入Spring Beans,首先需要在 pom.xml 中添加Mockito-core依赖项:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
</dependency>
先看看 Mockito 的简单使用:
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import static org.mockito.Mockito.*;
public class SimpleMockTest {
@Test
public void simpleTest() {
// 创建 Mock 对象
List<String> list = mock(List.class);
// 当... ... 时... ...
when(list.get(0)).thenReturn("hello");
when(list.get(1)).thenThrow(new RuntimeException());
String s = list.get(0);
Assert.assertEquals("hello", s);
}
}
运行起来,测试是通过的 。
接下来,我们来测试 Service 层,首先,提供 Service 层的接口 商品服务 ProductService,方法是获取商品名称:
public interface ProductService {
public String getProductName();
}
现在的场景是,有订单中心服务 OrderService 来调用商品服务 ProductService,先来实现ProductService 接口:
@Service
public class ProductServiceImpl implements ProductService{
public String getProductName() {
return "football";
}
}
订单中心服务 OrderService 接口及其实现类:
public interface OrderService {
public String getProductName();
}
public class OrderServiceImpl implements OrderService{
@Autowired
ProductService productService;
@Override
public String getProductName() {
return productService.getProductName();
}
}
假设 OrderService 和 ProductService 接口是两个人分开完成的,需要联调, 假设 ProductServiceImpl 的实现还没完成呢,这时就需要用 mock 模拟服务,先为 测试 配置 应用程序上下文, 将 @Profile(“test”) 注释用于 测试用例运行时 配置类:
@Profile("test")
@Configuration
public class ProductServiceConfiguration {
@Bean
@Primary
public ProductService productService() {
return Mockito.mock(ProductService.class);
}
}
接下来写测试方法:
@ActiveProfiles("test")
@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class SimpleMockTest {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
@Test
public void testProductName(){
when(productService.getProductName()).thenReturn("mock product name");
String productName= orderService.getProductName();
Assert.assertEquals("mock product name",productName);
}
运行结果:测试通过
如果在代码里写 System.out.println(orderService.getProductName());
在控制台可以看到,输出 “mock product name”。也就是说,这个 orderService 调用的 mock 出来的 productService 的 productService 方法的返回值,是 “mock product name”。在单元测试里
when(productService.getProductName()).thenReturn("mock product name");
的含义就是,调用的 mock 出来的 productService 的 productService 方法,会把这个方法的返回值设置为 “mock product name”。
二、Spring Boot 快速整合 MyBatis (去XML化)
去XML化 也就是 完全使用注解。
先导入依赖:
<!--添加Mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency> <!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency> <!--连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
在 application.yml 中进行 MyBatis 的属性配置,这样实体属性会根据驼峰命名法映射到表中:
## mybatis属性配置
mybatis:
configuration:
map-underscore-to-camel-case: true
1、基础注解
MyBatis 主要提供了以下 CRUD 注解:@Select、@Insert、@Update、@Delete,增删改查占据了绝大部分的业务操作,掌握这些基础注解的使用还是很有必要的。
先在 dao 层写一个 UserMapper :
@Mapper
@Repository
public interface UserMapper {
@Select("select * from person")
List<personEntry> list();
在 PersonService 接口中添加方法:
public List<personEntry> list();
在 PersonServiceImpl 中添加 UserMapper 对象和对接口方法的实现:
@Autowired
private UserMapper userMapper;
最后在 Contorller 层提供一个访问路径:
@GetMapping("/getUser")
public List<personEntry> getUser(){
List<personEntry> list= personService.list();
return list;
}
运行结果:
2、映射注解
Mybatis 还提供了这些映射注解:@Results 用于填写结果集的多个字段的映射关系;Result 用于填写结果集的单个字段的映射关系。我们可以在 查询SQL 的基础上,指定返回的结果集的映射关系,其中 property 表示实体对象的属性名,column 表示对应的数据库字段名。其实如果实体类的属性和数据库的字段可以通过驼峰命名法对应上,是不需要这俩注释的。但是如果出现了不对应的情况,比如,把 personEntry 类的属性 “gender” 改成 “sex”,想让 “sex” 属性能映射到数据库表中的 gender 字段。就需要在 UserMapper 里写的 list() 方法,在它上面用@Results 和 @Result 注解:
@Results({@Result(property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "age", column = "age"),
@Result(property = "sex", column = "gender")})
@Select("select * from person")
List<personEntry> list();
然后注意 批量处理那块儿 BatchConfig 类里的属性 “gender” 也是要改成 “sex” 的。运行代码,是没问题的。
这里提供一个快速生成映射结果集的方法:
public static String getResultsStr(Class origin) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("@Results({\n");
for (Field field : origin.getDeclaredFields()) {
String property = field.getName();
//映射关系:对象属性(驼峰)->数据库字段(下划线)
String column = new PropertyNamingStrategy.SnakeCaseStrategy().translate(field.getName())
.toUpperCase();
stringBuilder.append(String.format("@Result(property = \"%s\", column = \"%s\"),\n",
property, column));
} stringBuilder.append("})");
return stringBuilder.toString();
}
再给 personEntry 类里添加 phoneNum 和 personalID 属性,来看看下划线效果,主方法里测试一下这个方法:
public static void main(String[] args) {
System.out.println(getResultsStr(personEntry.class));
}
运行结果:
这种思路适合于数据库表中字段是大写字母+下划线命名,然后需要在 dao 层方法上加 @Results 和 @Result 注解的情况,就像刚刚演示过的那样:
3、高级注解
除了上面的基础注解、映射注解,MyBatis-3 还提供了 CRUD 的高级注解:@SelectProvider、@InsertProvider、@UpdateProvider 和 @DeleteProvider。
见名知意,这些高级注解主要用于 动态SQL,这里以 @SelectProvider 为例,主要包含两个注解属性,其中 type 表示工具类,method 表示工具类的某个方法,用于返回具体的 SQL。
先在 dao 层提供 UserSqlProvider 类,getBadUser 方法里拼接 SQL 语句,并返回 String 类型的 SQL :
public class UserSqlProvider {
public String getBadUser(String name, int age) {
SQL sql = new SQL();
sql = sql.SELECT("*").FROM("person");
if (name != null && age != 0) {
sql.WHERE("name=#{name} and age =#{age}");
}
// 永远不成立,查询无果
else {
sql.WHERE("1=2");
}
return sql.toString();
}
}
然后在 UserMapper 中添加代码,注意到 ,返回值并不是 String :
@SelectProvider(type = UserSqlProvider.class, method = "getBadUser")
public List<personEntry> getBadUser(String name,int age);
对应在 PersonService 接口中加方法:
public List<personEntry> getBadUser(String name,int age);
实现方法:
@Override
public List<personEntry> getBadUser(String name, int age) {
return userMapper.getBadUser(name,age);
}
在 Controller 层中提供访问路径:
@GetMapping("/getBadUser")
public List<personEntry> getBadUser(@RequestParam String name,@RequestParam int age){
return personService.getBadUser(name,age);
}
运行结果:
实现复杂关系映射之前我们可以在映射文件中通过配置来实现,在使用注解开发时我们需要借助 @Results 注解,@Result 注解,@One 注解,@Many 注解 等。
三、Spring Boot 保护 Web 应用程序
如果在类路径上添加了 Spring Boot Security (多数情况下还是用 Shiro 多)依赖项,则Spring Boot 应用程序会自动为所有 HTTP 端点提供基本身份验证。端点 “/” 和 “/home” 不需要任何身份验证。其他所有端点都需要身份验证。
要将 Spring Boot Security 添加到 Spring Boot 应用程序,需要在构建配置文件中添加 Spring Boot Starter Security 依赖项。
首先需要在 pom.xml 中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
在 template 包中添加以下 3 个 html:
- hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">您好,[[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="注销"/>
</form>
</body>
</html>
- home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security示例</title>
</head>
<body>
<h1>欢迎您!</h1>
<p>点击 <a th:href="@{/hello}">这里</a> 看到问候语.</p>
</body>
</html>
- login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security示例</title>
</head>
<body>
<div th:if="${param.error}">
无效的用户名和密码.
</div>
<div th:if="${param.logout}">
你已经注销.
</div>
<form th:action="@{/login}" method="post">
<div>
<label> 用户名 : <input type="text" name="username"/> </label>
</div>
<div>
<label> 密码: <input type="password" name="password"/> </label>
</div>
<div>
<input type="submit" value="登录"/>
</div>
</form>
</body>
</html>
想要实现这样的效果:最开始,访问 hello 页面,转到 login 页面,需要输入用户名和密码,如果验证登录成功了,继续转到 hello 路径,访问 hello 页面。如果没有登录成功,就只能访问 home 页面,再访问其他路径也只能回退到 login 登录页面。 login 和 home 是不拦截的。
首先,需要先为 home 和 hello 视图设置 Spring MVC - View 控制器。为此,创建一个扩展 WebMvcConfigurer 的 MVC 配置文件。
之前 InterceptorConfig 类实现了 WebMvcConfigurer 接口,所以就在这类中写方法:
// 设置 Controller 和 view 的映射关系
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 注册
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
接下来,创建一个 Web 安全配置文件,该文件用于保护应用程序以使用基本身份验证访问HTTP端点:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中验证,也可以用 userDetailsService 方法在数据库中验证
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("jia")
.password(new BCryptPasswordEncoder().encode("jia"))
.roles("user");
}
// 页面拦截
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/home")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
}
运行起来后,访问 hello 页面,转到 login 页面,然后输入正确的用户名和密码后:
点击”注销“:
主页 、 home 和 login 是不拦截的:
而且是要先登录成功了才能去访问 Controller 层的路径,否则都会回退到 /login 的。
四、总结
(1)MockMvc 是 Spring 框架里的东西,有了这个,就不需要去 浏览器 或者 postman 中访问路径,它会在控制台输出响应结果。mock 是模拟的意思,它会模拟方法的实现,Service 层的单元测试要引入 mockito 测试框架,使用 Mockito.mock(xxx.class)模拟,比如:
when(productService.getProductName()).thenReturn("mock product name");
就是说 当调用 productService.getProductName() 方法时,会返回 “mock product name”。
(2)MyBatis 提供了基础注解,也就是 CRUD 的几个操作 :@Select、@Insert、@Update、@Delete。还有 映射注解 @Results、@Result,把对象的属性映射成数据库字段。再有,高级注解,比如 @SelectProvider、@InsertProvider、@UpdateProvider 和 @DeleteProvider,主要用于 动态SQL。还有很多… … 活到老,学到s …
(3)Spring Boot 保护 Web 应用程序,需要继承 WebSecurityConfigureAdapter 类,使用@EnableWebSecurity 注解开启保护,可以选择在内存中验证,也可以在数据库中验证,并对相应路径设置拦截和放行。