目錄
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 {@Testvoid getAll() {}
}
這個時候,此方法是不能調用到任何單元測試的方法的,此類只生成了單元測試的框架類,具體的業務代碼要自己填充。
2. 添加單元測試代碼
- 在測試類上添加Spring Boot 框架測試注解: @SpringBootTest
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest // 表示當前單元測試的類是運行在 Spring Boot 環境中的(一定不能省略)
class UserMapperTest {// ..
}
- 添加單元測試業務邏輯
@Autowiredprivate UserMapper userMapper;@Testvoid 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
使用單元測試的方式去調用它.
@Testvoid 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>
測試:
@Testvoid 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.
先來看正常的用戶行為.
@Testvoid 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>
@Testvoid 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>
@Testvoid updatePassword() {int result = userMapper.updatePassword(1, "admin", "123456");System.out.println("修改: " + result);}
運行前后查詢數據庫,
可以看到, password已經成功修改了.
當再次修改newPassword參數的代碼時, 即:
int result = userMapper.updatePassword(1, "admin", "666666");
這里說明, 注入參數有問題, 代碼沒問題.
不過, 這里的測試是把原本數據庫污染了, 違背了單元測試的初衷, 那么要想不污染數據庫, 需要在測試類前加上@Transactional事務注解.
@Transactional // 事務@Testvoid 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@Testvoid 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>
@Testvoid 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>
@Testvoid 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());}