目录
1. Spring Boot单元测试
1.1 什么是单元测试?
1.2 单元测试有哪些好处?
1.3 Spring Boot 单元测试使用
单元测试的实现步骤
1. 生成单元测试类
2. 添加单元测试代码
简单的断言说明
2. Mybatis 单表增删改查
2.1 单表查询
2.2 参数占位符 ${} 和 #{}
${} 和 #{}的区别
1. 作用不同
2. 安全性: ${} 的SQL注入问题
${} 应用场景
2.3 单表修改操作
2.4 单表删除操作
2.5 单表添加操作
添加返回影响行数
添加返回影响行数和id
1. Spring Boot单元测试
1.1 什么是单元测试?
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证的过程就叫单元测试。
单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的(代码)功能是否正确。执行单元测试就是为了证明某段代码的执行结果是否符合我们的预期。如果测试结果符合我们的预期,称之为测试通过,否则就是测试未通过 (或者叫测试失败)
1.2 单元测试有哪些好处?
- 可以非常简单、直观、快速的测试某一个功能是否正确。
- 使用单元测试可以帮我们在打包的时候,发现一些问题,因为在打包之前,所有的单元测试必须通过, 否则不能打包成功。
- 使用单元测试,在测试功能的时候,可以不污染连接的数据库,也就是可以不对数据库进行任何改变的情况下,测试功能。
1.3 Spring Boot 单元测试使用
Spring Boot 项目创建时会默认单元测试框架 spring-boot-starter-test,而这个单元测试框架主要是依靠另个著名的测试框架 JUnit 实现的,打开 pom.xml 就可以看到,以下信息是 Spring Boot 项目创建是自动添加的:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
单元测试的实现步骤
1. 生成单元测试类
最终生成的代码:
package com.example.demo.mapper;
import org.junit.jupiter.api.Test;
class UserMapperTest {
@Test
void getAll() {
}
}
这个时候,此方法是不能调用到任何单元测试的方法的,此类只生成了单元测试的框架类,具体的业务代码要自己填充。
2. 添加单元测试代码
- 在测试类上添加Spring Boot 框架测试注解: @SpringBootTest
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest // 表示当前单元测试的类是运行在 Spring Boot 环境中的(一定不能省略)
class UserMapperTest {
// ..
}
- 添加单元测试业务逻辑
@Autowired
private UserMapper userMapper;
@Test
void getAll() {
List<UserEntity> list = userMapper.getAll();
System.out.println(list.size());
}
简单的断言说明
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
断言: 如果断言失败,则后面的代码都不会执行.
2. Mybatis 单表增删改查
2.1 单表查询
下面我们来实现一下根据用户id查询用户信息的功能.
在UserMapper类中添加接口:
// 根据 id 查询用户对象
UserEntity getUserById(@Param("uid") Integer id); // @Param是给形参起名
<select id="getUserById" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where id=${uid}
</select>
注: 上面 ${uid} 中的uid对应@Param的uid
使用单元测试的方式去调用它.
@Test
void getUserById() {
UserEntity user = userMapper.getUserById(2);
System.out.println(user);
}
那么我们的预期结果是能够打印出数据库中"zhangsan"的数据:
执行结果:
可以看到, 预期结果成功执行了.
2.2 参数占位符 ${} 和 #{}
Mybatis获取动态参数有两种实现:
- ${paramName} -> 直接替换
- #{paramName} -> 占位符模式
验证直接替换:
在Spring配置文件中有一个配置, 只需要把这个配置给它配置之后, 那么Mybatis的执行SQL(Mybatis底层是基于JDBC), 最终会生成JDBC的执行SQL和它的执行模式, 那么我们就可以把这个执行的SQL语句打印出来.
需要配置两个配置项, 一个是日志打印的实现, 另一个是设置日志打印的级别 (SQL的打印默认输出的级别的debug级别, 但日志默认级别的info, 默认info要大于debug, 所以并不会显示, 所以要去进行日志级别的设置).
# 打印 Mybatis 执行 SQL
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
logging.level.com.example.demo=debug
配置完成之后再次运行刚才的测试代码, 可以看到SQL的相关信息都被打印了出来, 所以可以知道$是直接替换的模式.
将上文的 $ 换成 # , 会看到的是, SQL语句的id变成了?, 也就是变成了占位符的模式.
而占位符的模式是预执行的, 而预执行是比较安全的, 具体来说预执行可以有效的排除SQL注入的问题.
${} 和 #{}的区别
1. 作用不同
${} 所见即所得, 直接替换, #{} 是预处理的.
在进行使用的时候, 如果传的是int这种简单数据类型的时候, 两者是没有区别的, 但是如果更复杂点的使用varchar, 就会有安全的问题出现.
在UserMapper类中添加接口:
// 根据名称查询用户对象
UserEntity getUserByUserName(@Param("username") String username);
<select id="getUserByUserName" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username=#{username}
</select>
测试:
@Test
void getUserByUserName() {
UserEntity user = userMapper.getUserByUserName("zhangsan");
System.out.println(user);
}
测试结果没有问题, 那么再将#换成$.
<select id="getUserByUserName" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username=${username}
</select>
这时程序报错没有找到'zhangsan', 并且我们看到SQL语句变成了.
在数据库客户端中执行图中SQL语句也是会报出和上图一样的错.
那么这里的原因就在于刚才我们的代码中, ${}是直接替换的模式, 当加上单引号后再次运行就正常运行了.
<select id="getUserByUserName" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username='${username}'
</select>
但是加单引号只能保证不报错, 但是不能保证安全性问题.
所以当我们遇到是int类型的时候, ${} 和 #{} 在执行上没有什么区别, 当出现字符型的时候${} 就有可能会出现问题.
2. 安全性: ${} 的SQL注入问题
${} 的安全性问题出现在登录, 接下来我们以登录为例看一下什么是SQL注入.
首先SQL注入是 用户用了并不是一个真实的用户名和密码, 但是却查询到了数据. 我们通过代码说明.
// 登录方法
UserEntity login(UserEntity user);
<select id="login" resultType="com.example.demo.entity.UserEntity">
select * from userinfo where username='${username}' and password='${password}'
</select>
注: 当Interface传的是对象时, xml中获取属性时, 也就是{}里面直接写对象的属性名即可, 无需"对象.属性", 这是Mybatis的约定
为了演示效果, 我们在数据库中删掉id=2的zhangsan.
先来看正常的用户行为.
@Test
void login() {
String username = "admin";
String password = "admin";
UserEntity inputUser = new UserEntity();
inputUser.setUsername(username);
inputUser.setPassword(password);
UserEntity user = userMapper.login(inputUser);
System.out.println(user);
}
可以看到, 找到了相关信息.
当输入错误密码时, 即:
String password = "admin2";
可以看到, 结果是null, 以上都是正常的行为.
接下来我们来看一个特殊的, 不正常的行为, 输入如下密码:
String password = "' or 1='1";
此时我们可以发现, 输入了一个不正常的密码, 却把admin查出来了, 这就是SQL注入, 对于程序来说是非常危险的.
那么我们可以看到这里的SQL语句是
select * from userinfo where username='admin' and password='' or 1='1'
所以这便是这里出错的原因, 它把字符串误解析成SQL指令去执行了, 使逻辑运行结果与预期不同, 但却正常执行.
当把 ${} 改为 #{} 后, 再次测试, 可以看到结果是null.
由上可见, 使用 ${} 是会安全性问题的, 而使用 #{} 就不会出现安全性问题, 原因在于 #{} 使用了JDBC的占位符的模式, 那么这种模式是预执行的, 是直接当成字符串来执行的.
${} 应用场景
${} 虽然在查询的时候会有安全性问题, 但是它也有具体的应用场景, 比如以下场景:
在淘宝中有时候需要按照某种属性进行排序, 比如价格低到高或者高到低, 这时SQL传递的就是order by后的规则asc或desc.
使用 ${sort} 可以实现排序查询,而使用 #{sort} 就不能实现排序查询了,因为当使用 #{sort} 查询时如果传递的值为 String 则会加单引号,就会导致 sql 错误。
那么对于我们之前的程序, 我们也可以进行类似的应用.
List<UserEntity> getAllByIdOrder(@Param("ord") String order);
<select id="getAllByIdOrder" resultType="com.example.demo.entity.UserEntity">
select * from userinfo order by id ${ord}
</select>
@Test
void getAllByIdOrder() {
List<UserEntity> list = userMapper.getAllByIdOrder("desc");
System.out.println(list.size());
}
这时使用 #{} 就会报错了.
<select id="getAllByIdOrder" resultType="com.example.demo.entity.UserEntity">
select * from userinfo order by id #{ord}
</select>
既然 ${} 有用, 但是它也极其的危险, 在使用的时候要注意, 要保证它的值必须得被枚举. 所以尽量少用.
2.3 单表修改操作
比如需要修改用户密码.
首先, 在Interface声明方法,
// 修改密码
int updatePassword(@Param("id") Integer id,
@Param("password") String password,
@Param("newPassword") String newPassword);
然后在xml中实现方法, 注意修改操作是使用<update>标签.
<update id="updatePassword">
update userinfo set password=#{newPassword}
where id=#{id} and password=#{password}
</update>
@Test
void updatePassword() {
int result = userMapper.updatePassword(1, "admin", "123456");
System.out.println("修改: " + result);
}
运行前后查询数据库,
可以看到, password已经成功修改了.
当再次修改newPassword参数的代码时, 即:
int result = userMapper.updatePassword(1, "admin", "666666");
这里说明, 注入参数有问题, 代码没问题.
不过, 这里的测试是把原本数据库污染了, 违背了单元测试的初衷, 那么要想不污染数据库, 需要在测试类前加上@Transactional事务注解.
@Transactional // 事务
@Test
void updatePassword() {
int result = userMapper.updatePassword(1, "123456", "666666");
System.out.println("修改: " + result);
}
当加上注解之后, 测试的代码可以正常执行, 但是就不会污染数据库了.
看到打印了"修改: 1", 就说明成功修改了.
在代码执行的时候不会进行干扰的, 只不过在执行之初, 会开启一个事务, 等全部代码执行完了, 比如这里的"修改: x"已经正常打印了, 然后在它执行完会进行rollback回滚操作, 所以就不会污染数据库了.
验证数据库是否污染:
2.4 单表删除操作
// 删除用户
int delById(@Param("id") Integer id);
<delete id="delById">
delete from userinfo where id=#{id}
</delete>
@Transactional
@Test
void delById() {
int id = 1;
int result = userMapper.delById(id);
System.out.println("删除结果: " + result);
}
2.5 单表添加操作
添加返回影响行数
// 添加用户
int addUser(UserEntity user);
<insert id="addUser">
insert into userinfo(username,password) values(#{username},#{password})
</insert>
@Test
void addUser() {
UserEntity user = new UserEntity();
user.setUsername("lisi");
user.setPassword("123456");
int result = userMapper.addUser(user);
System.out.println("添加: " + result);
}
添加返回影响行数和id
int addUserGetId(UserEntity user);
<insert id="addUserGetId" useGeneratedKeys="true" keyProperty="id">
insert into userinfo(username,password) values(#{username},#{password})
</insert>
@Test
void addUserGetId() {
UserEntity user = new UserEntity();
user.setUsername("lili");
user.setPassword("123456");
int result = userMapper.addUserGetId(user);
System.out.println("添加结果: " + result);
System.out.println("ID: " + user.getId());
}