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

news2024/11/24 5:22:13

目录

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 {

    @Test
    void getAll() {

    }
}

这个时候,此方法是不能调用到任何单元测试的方法的,此类只生成了单元测试的框架类,具体的业务代码要自己填充。

2. 添加单元测试代码

  1. 在测试类上添加Spring Boot 框架测试注解: @SpringBootTest
import org.springframework.boot.test.context.SpringBootTest;

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

  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>

测试:

    @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());
    }

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

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

相关文章

LeetCode 周赛上分之旅 #39 结合中心扩展的单调栈贪心问题

⭐️ 本文已收录到 AndroidFamily&#xff0c;技术和职场问题&#xff0c;请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问。 学习数据结构与算法的关键在于掌握问题背后的算法思维框架&#xff0c;你的思考越抽象&#xff0c;它能覆盖的问题域就越广&#xff0c;理解难度…

赴日IT培训 国内的程序员去日本做IT容易吗?

去日本当程序员的两大要素就是技术和日语。所以说容易也容易&#xff0c;如果学历过关&#xff08;统招大专以上&#xff09;&#xff0c;再加上在国内有经验&#xff0c;所以技术方面问题不大。要说难也难&#xff0c;你要克服语言关&#xff0c;去本本工作对日语的要求比较高…

NeuralNLP-NeuralClassifier的使用记录(一),训练预测自己的【英文文本多分类】

NeuralNLP-NeuralClassifier的使用记录&#xff0c;训练预测自己的英文文本多分类 NeuralNLP-NeuralClassifier是腾讯开发的一个多层多分类应用工具&#xff0c;支持的任务包括&#xff0c;文本分类中的二分类、多分类、多标签&#xff0c;以及层次多标签分类。支持的文本编码…

运维工程师常见面试题

1、http常见返回码 2、mysql的同步方式 1&#xff09;异步复制 MySQL默认的复制即是异步的&#xff0c;主库在执行完客户端提交的事务后会立即将结果返给给客户端&#xff0c;并不关心从库是否已经接收并处理&#xff0c;这样就会有一个问题&#xff0c;主如果crash掉了&a…

207、仿真-51单片机脉搏心率与血氧报警Proteus仿真设计(程序+Proteus仿真+配套资料等)

毕设帮助、开题指导、技术解答(有偿)见文未 目录 一、硬件设计 二、设计功能 三、Proteus仿真图 四、程序源码 资料包括&#xff1a; 需要完整的资料可以点击下面的名片加下我&#xff0c;找我要资源压缩包的百度网盘下载地址及提取码。 方案选择 单片机的选择 方案一&a…

Docker部署rabbitmq遇到的问题 Stats in management UI are disabled on this node

1. Stats in management UI are disabled on this node #进入rabbitmq容器 docker exec -it {rabbitmq容器名称或者id} /bin/bash#进入容器后&#xff0c;cd到以下路径 cd /etc/rabbitmq/conf.d/#修改 management_agent.disable_metrics_collector false echo management_age…

ArcGIS Maps SDK for JavaScript系列之二:认识Map和MapView

目录 Map创建一个 Map 对象的示例代码&#xff1a;Map的常用属性Map的常用方法 MapViewMapView的常用属性MapView的常用方法 在 ArcGIS Maps SDK for JavaScript 中&#xff0c;Map 和 MapView 是两个重要的概念&#xff0c;用于创建和展示地图应用程序。 Map Map 表示一个地图…

2023-08-13 LeetCode每日一题(合并两个有序数组)

2023-08-13每日一题 一、题目编号 88. 合并两个有序数组二、题目链接 点击跳转到题目位置 三、题目描述 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2&#xff0c;另有两个整数 m 和 n &#xff0c;分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 …

Error: EACCES: permission denied, rename ‘/usr/local/lib/node_modules/appium‘

在使用npm uninstall -g appium卸载appium的过程中报错 Error: EACCES: permission denied, rename /usr/local/lib/node_modules/appium -> /usr/local/lib/node_modules/.appium-cfBVovI6 npm ERR! code EACCES npm ERR! syscall rename npm ERR! path /usr/local/lib/n…

gcc/g++ 编译选项详解

gcc/g 编译选项详解 文章目录 gcc/g 编译选项详解编译步骤gcc 与 g 区别gcc 命令的常用选项编译优化选项-O 优化-O1优化-O2-O0-Os-Ofast-Og-Oz-O 选项控制特定的优化 WarningsReference>>>>> 欢迎关注公众号【三戒纪元】 <<<<< 编译步骤 gcc 、…

Codeforces Round 891 (Div. 3)ABC

Codeforces Round 891 (Div. 3) 目录 A. Array Coloring题目大意思路代码 B. Maximum Rounding题目大意思路代码 C. Assembly via Minimums题目大意思路代码 A. Array Coloring 题目大意 给你一个包含 n n n个数字的数组&#xff0c;你的任务是判断这个数组是否可以划分成两个…

TCP/IP 下的计算机网络江湖

〇、引言 在当今数字化时代,计算机网络宛如广袤江湖,涵盖着五大门派:物理层、数据链路层、网络层、传输层和应用层。每个门派独具技能,共同构筑着现代网络的框架。物理层宛如江湖基石,将比特流传输;数据链路层如武林传承,组织数据帧传递;网络层则像导航大师,寻找传送路…

Web framework-Gin

一、Gin Go Web--Go Module 软件框架&#xff08;software framework&#xff09;&#xff0c;通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范&#xff0c;也指为了实现某个软件组件规范时&#xff0c;提供规范所要求之基础功能的软件产品。 框架就是&#…

【报童模型】随机优化问题二次规划

面对需求的不确定性&#xff0c;报童模型是做库存优化的常见模型。而标准报童模型假设价格是固定的&#xff0c;此时求解一个线性规划问题&#xff0c;可以得到最优订货量&#xff0c;这种模型存在局限性。因为现实世界中价格与需求存在一定的关系&#xff0c;本文假设需求q是价…

FreeRTOS源码分析-12 低功耗管理

目录 1 STM32低功耗管理概念及应用 1.1睡眠模式 1.2 停止模式 1.3 待机模式 2 Tickless低功耗管理 2.1 Tickless低功耗模式介绍 2.2 FreeRTOS低功耗模式配置 2.3 FreeRTOS低功耗模式应用 3 低功耗管理实际项目开发 3.1 低功耗设计必须要掌握的硬件知识 …

leetcode 1049. 最后一块石头的重量 II

2023.8.13 与分割等和子集类似&#xff0c;可以转化为0-1背包问题。 本题也是需要将数组元素分成两堆&#xff0c;区别在于本题需要使这两堆的差值最小&#xff0c;而之前那题是需要两堆差值为0。 使用之前的一维dp数组的思路&#xff0c;代码如下&#xff1a; class Solution…

开发一个RISC-V上的操作系统(八)—— 抢占式多任务(Preemptive Multitasking)

目录 一、抢占式多任务 二、代码实现 三、上板测试 本节的代码在仓库的 06_Preemptive_Muti_Task 目录下&#xff0c;仓库链接&#xff1a;riscv_os: 一个RISC-V上的简易操作系统 本文代码的运行调试会在前面开发的RISC-V处理器上进行&#xff0c;仓库链接&#xff1a;cpu_…

heap pwn 入门大全 - 1:glibc heap机制与源码阅读(上)

本文为笔者学习heap pwn时&#xff0c;学习阅读glibc ptmalloc2源码时的笔记&#xff0c;与各位分享。可能存在思维跳跃或错误之处&#xff0c;敬请见谅&#xff0c;欢迎在评论中指出。本文也借用了部分外网和其他前辈的素材图片&#xff0c;向各位表示诚挚的感谢&#xff01;如…

MySQL修改编码

插入中文乱码,我电脑上没有出现&#xff0c;我只是来记录一下 MySQL版本: 8.0.34Windows 查看编码 mysql中输入下面的命令 show variables like ‘character_%’; 乱码的一般是图片中划红线的部分不是utf8 character_set_database是设置数据库的默认编码的 character_set_ser…

面试热题(回文链表)

给定一个链表的 头节点 head &#xff0c;请判断其是否为回文链表。 如果一个链表是回文&#xff0c;那么链表节点序列从前往后看和从后往前看是相同的。 回文链表类似于回文串&#xff0c;正读倒读的顺序是一样的&#xff0c;那么我们怎么去判断一个链表是否是回文链表呢&…