Spring Boot单元测试与Mybatis单表增删改查

目录

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. 可以非常简单、直观、快速的测试某一个功能是否正确。
  2. 使用单元测试可以帮我们在打包的时候,发现一些问题,因为在打包之前,所有的单元测试必须通过, 否则不能打包成功。

  1. 使用单元测试,在测试功能的时候,可以不污染连接的数据库,也就是可以不对数据库进行任何改变的情况下,测试功能。

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. 添加单元测试代码

  1. 在测试类上添加Spring Boot 框架测试注解: @SpringBootTest
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest // 表示当前单元测试的类是运行在 Spring Boot 环境中的(一定不能省略)
class UserMapperTest {// ..
}
  1. 添加单元测试业务逻辑
    @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获取动态参数有两种实现:

  1. ${paramName} -> 直接替换
  2. #{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());}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/34830.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学点Selenium玩点新鲜~,让分布式测试有更多玩法

前 言 我们都知道 Selenium 是一款在 Web 应用测试领域使用的自动化测试工具&#xff0c;而 Selenium Grid 是 Selenium 中的一大组件&#xff0c;通过它能够实现分布式测试&#xff0c;能够帮助团队简单快速在不同的环境中测试他们的 Web 应用。 分布式执行测试其实并不是一…

小米平板6Max14即将发布:自研G1 电池管理芯片,支持33W反向快充

明天晚上7点&#xff08;8 月 14 日&#xff09;&#xff0c;雷军将进行年度演讲&#xff0c;重点探讨“成长”主题。与此同时&#xff0c;小米将推出一系列全新产品&#xff0c;其中包括备受瞩目的小米MIX Fold 3折叠屏手机和小米平板6 Max 14。近期&#xff0c;小米官方一直在…

分布式搜索ElasticSearch-ES(一)

一、ElasticSearch介绍 ES是一款非常强大的开源搜索引擎&#xff0c;可以帮我们从海量的数据中快速找到我们需要的内容。 ElasticSearch结合kibana、Logstash、Beats&#xff0c;也就是elastic stack(ELK)&#xff0c;被广泛运用在日志数据分析&#xff0c;实时监控等领域。 …

Ajax 笔记(二)—— Ajax 案例

笔记目录 2. Ajax 综合案例2.1 案例一-图书管理2.1.1 渲染列表2.1.2 新增图书2.1.3 删除图书2.1.4 编辑图书 2.2 案例二-背景图的上传和更换2.2.1 上传2.2.2 更换 2.3 案例三-个人信息设置2.3.1 信息渲染2.3.2 头像修改2.2.3 信息修改2.3.4 提示框 Ajax 笔记&#xff1a; Ajax…

React Native 列表组件基础知识

ScrollView 组件 ScrollView组件是一个容器滚动组件&#xff0c;当容器超出指定宽高时就可以进行滚动交互。 ScrollView组件是一次性渲染所有的 React 子组件&#xff0c;这在性能上是比较差的&#xff0c;所以不建议当列表特别长的时候使用此组件。 接下来列举几个常用的一…

HTML(JavaEE初级系列12)

目录 前言&#xff1a; 1.HTML结构 1.1认识HTML标签 1.2HTML文件基本结构 1.3标签层次结构 1.4快速生成代码框架 2.HTML常见标签 2.1注释标签 2.2标题标签&#xff1a;h1-h6 2.3段落标签&#xff1a;p 2.4换行标签&#xff1a; br 2.5格式化标签 2.6图片标签&#…

【详细教程】学会使用Python隧道代理

作为一名专业爬虫程序猿&#xff0c;我深知在进行网络数据采集时&#xff0c;可能会面临网络封锁、隐私泄露等问题。今天&#xff0c;我将与大家分享如何学会使用Python隧道代理&#xff0c;帮助我们自由访问受限网站&#xff0c;同时保护了解探索Python隧道代理&#xff01; …

3.1 Spring MVC概述

1. MVC概念 MVC是一种编程思想&#xff0c;它将应用分为模型&#xff08;Model&#xff09;、视图&#xff08;View&#xff09;、控制器&#xff08;Controller&#xff09;三个层次&#xff0c;这三部分以最低的耦合进行协同工作&#xff0c;从而提高应用的可扩展性及可维护…

解开谜团:为什么红黑树胜过AVL树?

为什么红黑树胜过AVL树 博主简介一、引言1.1、红黑树和AVL树简介1.2、红黑树在某些方面优于AVL树 二、红黑树和AVL树的基本原理2.1、红黑树的定义和性质2.2、AVL树的定义和性质2.3、对比两种树结构的特点 三、插入和删除操作的复杂性比较3.1、红黑树的插入操作和平衡性维护3.2、…

【Bert101】变压器模型背后的复杂数学【02/4】

一、说明 众所周知&#xff0c;变压器架构是自然语言处理&#xff08;NLP&#xff09;领域的突破。它克服了 seq-to-seq 模型&#xff08;如 RNN 等&#xff09;无法捕获文本中的长期依赖性的局限性。变压器架构被证明是革命性架构&#xff08;如 BERT、GPT 和 T5 及其变体&…

【阵列信号处理】空间匹配滤波器、锥形/非锥形最佳波束成形器、样本矩阵反演 (SMI) 研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

关于使用pycharm遇到只能使用unittest方式运行,无法直接选择Run

相信大家可能都遇到过这个问题&#xff0c;使用pycharm直接运行脚本的时候&#xff0c;只能选择unittest的方式&#xff0c;能愁死个人 经过几次各种尝试无果之后&#xff0c;博主就放弃死磕了&#xff0c;原谅博主是个菜鸟 后来遇到这样的问题&#xff0c;往往也就直接使用cm…

Python爬虫-抓取的目标数据为#x开头,怎么解决?

前言 本文是该专栏的第4篇,后面会持续分享python爬虫案例干货,记得关注。 在做爬虫项目的时候,有时候抓取的平台目标数据为&#x开头,如下图所示: 浏览器显示的正常数据,但通过爬虫协议获取到的网页源码数据却是以&#x开头的隐藏数据,遇到这种情况,爬虫需要怎么处…

【Linux从入门到精通】文件I/O操作(C语言vs系统调用)

文章目录 一、C语言的文件IO相关函数操作 1、1 fopen与fclose 1、2 fwrite 1、3 fprintf与fscanf 1、4 fgets与fputs 二、系统调用相关接口 2、1 open与close 2、2 write和read 三、简易模拟实现cat指令 四、总结 &#x1f64b;‍♂️ 作者&#xff1a;Ggggggtm &#x1f64b;‍…

Golang bitset 基本使用

安装&#xff1a; go get github.com/bits-and-blooms/bitset下面代码把fmtx换成fmt就行 //------------基本操作------------//构建一个64bit长度的bitsetb : bitset.New(64)//放入一个数b.Set(10)fmtx.Println("add-10&#xff1a;", b.DumpAsBits()) // 0000000…

针对英特尔酷睿 CPU 优化,Canonical 发布 Ubuntu 实时内核

导读Canonical 今天宣布针对支持时序协调运算&#xff08;TCC&#xff09;和时间敏感网络&#xff08;IEEE TSN&#xff09;的英特尔酷睿处理器&#xff0c;推出优化版实时 Ubuntu 内核。 Canonical 于今年 2 月宣布&#xff0c;为购买 Ubuntu Pro 订阅&#xff0c;使用代号为 …

ESP8266(RTOS SDK)内嵌网页以实现WEB配网以及数据交互

【本文发布于https://blog.csdn.net/Stack_/article/details/131997098&#xff0c;未经允许不得转载&#xff0c;转载须注明出处】 1、执行make menuconfig&#xff0c;将http头由512改为更大的值&#xff0c;否则用电脑浏览器访问正常&#xff0c;但用手机浏览器访问会因为ht…

【快应用】list组件如何区分滑动的方向?

【关键词】 list组件、滑动方向、scroll 【问题背景】 有cp反馈list这个组件在使用的时候&#xff0c;不知道如何区分它是上滑还是下滑。 【问题分析】 list组件除了通用事件之外&#xff0c;还提供了scroll、scrollbottom、scrolltop、scrollend、scrolltouchup事件&#x…

九、多态(1)

本章概要 向上转型回顾 忘掉对象类型 转机 方法调用绑定产生正确的行为可扩展性陷阱&#xff1a;“重写”私有方法陷阱&#xff1a;属性与静态方法 多态是面向对象编程语言中&#xff0c;继数据抽象和继承之外的第三个重要特性。 多态提供了另一个维度的接口与实现分离&…

C++_模板初阶

在面向对象中&#xff0c;我们可以使用重载来实现多态。 但是问题在于&#xff0c;重载的函数仅仅是类型不同&#xff0c;代码复用率比较低&#xff0c;只要有新的类型出现时&#xff0c;就要增加对应的函数&#xff1b;另一方面它的代码可维护性比较低&#xff0c;一个出错可…