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

2.6 like查询

2.7 标签返回类型使用背景使用

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

2.6 like查询

like使用 #{} 会报错

    // 根据用户名模糊查询
    List<UserEntity> getListByName(@Param("username") String username);
    <select id="getListByName" resultType="com.example.demo.entity.UserEntity">
        select * from userinfo where username like '%#{username}%'
    </select>
    @Test
    void getListByName() {
        String username = "zhang";
        List<UserEntity> list = userMapper.getListByName(username);
        list.stream().forEach(System.out::println);
    }

可以看到, 当我们使用#{}的方式进行like查询的时候, 它是有问题的, 因为它会把#{username}作为占位符, 看到传过来的参数是String后, 相当于又加了一个单引号, 也就是'%'zhang'%', 所以它是报错的.

在数据库中演示一下.

那么可以通过MySQL内置方法concat解决这个问题, concat能实现字符的拼接.

    <select id="getListByName" resultType="com.example.demo.entity.UserEntity">
        select * from userinfo where username like concat('%',#{username},'%')
    </select>

可以看到, 我们可以查到zhangsan的信息了, 说明此时查询是没有问题的.


2.7 <select>标签返回类型

我们知道, <select>查询标签至少需要设置两个属性:

id属性: 用于标识实现接口中的哪个方法;

结果映射(即返回)属性: 结果映射有两种实现标签: <resultType>和<resultMap>

<resultType>

绝大多数场景可以使用resultType返回, 如下所示:

    <select id="getAll" resultType="com.example.demo.entity.UserEntity">
        select * from userinfo
    </select>

它的优点是使用方便,直接定义到某个实体类即可.

<resultMap>

<resultMap>使用场景:

  • 字段名称和程序中的属性名不同的情况,可使用 resultMap 配置映射
  • 一对一和一对多关系可以使用 resultMap 映射并查询数据

使用背景

我们以数据库中的字段为例,

比如当项目实体类中密码的属性为pwd, 而数据库中对应的字段为password, 这个时候Mybatis就匹配不到了, 原本实体类中为password, Mybatis因为做了映射, 而映射规则是根据名称进行匹配, 所以能够拿到数据库中的信息(Mybatis是一个ORM框架). 那么问题在哪里呢?

我们来看前文的getListByName(), 原来使用zhang把信息查询到了,

但是我们此时将实体类中的password改为pwd, 再次测试,

我们可以会发现, 执行没有报错, JDBC可以正常拿到password, 但是在映射的时候给pwd没有映射上.

这里就是因为对比的时候pwd和password是不相同的, Mybatis在属性中没有找到pwd.

所以这时我们<resultType>使用实体类已经和数据库的表对应不上了, 那么就使用<resultMap>.


使用

    <resultMap id="BaseMap" type="com.example.demo.entity.UserEntity">
        <id property="id" column="id"></id>
        <result property="username" column="username"></result>
        <result property="pwd" column="password"></result>
        <result property="createtime" column="createtime"></result>
        <result property="updatetime" column="updatetime"></result>
    </resultMap>

    <select id="getListByName" resultMap="BaseMap">
        select * from userinfo where username like concat('%',#{username},'%')
    </select>

可以看到, 此时pwd是有值的, 数据库中为password, 程序中是pwd, 依然能够拿到值.


注: 如果依然要用resultType就需要对SQL语句修改.

    <select id="getListByName" resultType="com.example.demo.entity.UserEntity">
        select id,username,password as pwd from userinfo where username like concat('%',#{username},'%')
    </select>

可以看到成功的拿到了password, 并且在SQL中是实现了重命名.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

VGG分类实战:猫狗分类

关于数据集 数据集选择的是Kaggle上的Cat and Dog&#xff0c;猫狗图片数量上达到了上万张。你可以通过这里进入Kaggle下载数据集Cat and Dog | Kaggle。 在我的Github仓库当中也放了猫狗图片各666张。 VGG网络 VGG的主要特点是使用了一系列具有相同尺寸 3x3 大小的卷积核进…

数据结构之动态内存管理机制

目录 数据结构之动态内存管理机制 占用块和空闲块 系统的内存管理 可利用空间表 分配存储空间的方式 空间分配与回收过程产生的问题 边界标识法管理动态内存 分配算法 回收算法 伙伴系统管理动态内存 可利用空间表中结点构成 分配算法 回收算法 总结 无用单元收…

PyTorch翻译官网教程-LANGUAGE MODELING WITH NN.TRANSFORMER AND TORCHTEXT

官网链接 Language Modeling with nn.Transformer and torchtext — PyTorch Tutorials 2.0.1cu117 documentation 使用 NN.TRANSFORMER 和 TORCHTEXT进行语言建模 这是一个关于训练模型使用nn.Transformer来预测序列中的下一个单词的教程。 PyTorch 1.2版本包含了一个基于论…

判断推理

六哥爱学习呀 产品经理 不是说我努力学习我就一定可以通过考试&#xff0c;所以是推不出&#xff0c;类似数学中充分必要性 8 回复 发布于 2019-08-07 16:28 官方解析&#xff1a; 当丙的范围足够大时&#xff0c;可能与甲相交或完全包含甲&#xff0c;在此情况下&#xff0c;有…

Python方式实现射后不管导弹的简易制导系统

1 问题 对QN-506上的S570智能反坦克制导导弹的射后不管产生了浓厚的兴趣&#xff0c;想用Python简易还原一下。 2 方法 之前查阅资料时了解到使用pygame库制作的贪吃蛇&#xff0c;是否有一种方法能让“贪吃蛇”一直跟着鼠标走呢&#xff1f;鼠标模拟行进中的坦克&#xff0c;“…

Linux 常见问题解决思路

Linux 常见问题解决思路 CPU 高系统平均负载高&#xff08;load average&#xff09; CPU 高 1&#xff0c;步骤&#xff1a;查找进程-》查找线程-》分析threadDump日志-》找出问题代码 a、查看 cpu 高的 java 进程 topb、生成进程下所有线程的栈日志 jstack 1721 > 1712.…

设计模式之单例设计模式

单例设计模式 2.1 孤独的太阳盘古开天&#xff0c;造日月星辰。2.2 饿汉造日2.3 懒汉的队伍2.4 大道至简 读《秒懂设计模式总结》 单例模式(Singleton)是一种非常简单且容易理解的设计模式。顾名思义&#xff0c;单例即单一的实例&#xff0c;确切地讲就是指在某个系统中只存在…

centos 7.9 部署django项目

1、部署框架 主要组件&#xff1a;nginx、uwsgi、django项目 访问页面流程&#xff1a;nginx---》uwsgi---》django---》uwsgi---》nginx 2、部署过程 操作系统&#xff1a;centos 7.9 配置信息&#xff1a;4核4G 50G 内网 eip &#xff1a;10.241.103.216 部署过程&…

pycharm调整最大堆发挥最大

python程序运行时&#xff0c;怎么提高效率&#xff0c;设置pycharm最大堆过程如下&#xff1b; 一、进入设置pycharm最大堆&#xff1b; 二、进入设置pycharm最大堆&#xff1b; 如果8g设置为6g左右&#xff0c;占75%左右最佳

数学建模之“TOPSIS数学模型”原理和代码详解

一、简介 TOPSIS&#xff08;Technique for Order Preference by Similarity to Ideal Solution&#xff09;是一种多准则决策分析方法&#xff0c;用于解决多个候选方案之间的排序和选择问题。它基于一种数学模型&#xff0c;通过比较每个候选方案与理想解和负理想解之间的相…

小程序CSS button按钮自定义高度之后不居中

问题&#xff1a; 按钮设置高度后不居中 <view><button class"btn1" size"">Save</button> </view> page {font-size: 30rpx; }.btn1 {margin-top: 100rpx;height: 190rpx;background: linear-gradient(90deg, #FF8A06, #FF571…

c语言每日一练(8)

前言&#xff1a;每日一练系列&#xff0c;每一期都包含5道选择题&#xff0c;2道编程题&#xff0c;博主会尽可能详细地进行讲解&#xff0c;令初学者也能听的清晰。每日一练系列会持续更新&#xff0c;暑假时三天之内必有一更&#xff0c;到了开学之后&#xff0c;将看学业情…

微信小程序:模板使用

目录 模板的优点&#xff1a; 一、静态模板创建 二、静态模板使用 1.*.wxml引入模板 2.模板使用 3.*.wxss引入模板的样式 三、动态模板创建 四、动态模板使用 1.*.wxml引入模板 2.模板使用 3.*.js定义动态数据 五、结果展示 总结 模板的优点&#xff1a; 有利于保持网…

NVIDIA Omniverse与GPT-4结合生成3D内容

全球各行业对 3D 世界和虚拟环境的需求呈指数级增长。3D 工作流程是工业数字化的核心&#xff0c;开发实时模拟来测试和验证自动驾驶车辆和机器人&#xff0c;操作数字孪生来优化工业制造&#xff0c;并为科学发现铺平新的道路。 如今&#xff0c;3D 设计和世界构建仍然是高度…

HDFS原理剖析

一、概述 HDFS是Hadoop的分布式文件系统&#xff08;Hadoop Distributed File System&#xff09;&#xff0c;实现大规模数据可靠的分布式读写。HDFS针对的使用场景是数据读写具有“一次写&#xff0c;多次读”的特征&#xff0c;而数据“写”操作是顺序写&#xff0c;也就是…

免费开源服务器资源监控系统grafana+prometheus+node_exporter

有项目做测试的时候需要查询服务器资源利用情况&#xff0c;自己又没写相应的模块&#xff0c;此时就需要一套好用的资源监控系统&#xff0c;&#xff0c;咨询了运维人员给推荐了一套&#xff0c;装完后真的很好用。 就是grafanaprometheusnode_exporter&#xff08;linux&am…

DAY3,ARM(LED点灯实验)

1.汇编实现开发板三盏灯点亮熄灭&#xff1b; .text .global _start _start: /**********LED123点灯**************/RCC_INIT:1使能PE10 PF10 PE8RCC..寄存器,E[4]1 F[5]1 0x50000a28ldr r0,0x50000a28ldr r1,[r0]orr r1,r1,#(0x3 << 4)str r1,[r0]LED1_INET:2初始化LED…

Vue3实现图片懒加载及自定义懒加载指令

Vue3实现图片懒加载及自定义懒加载指令 前言1.使用vue3-lazyload插件2.自定义v-lazy懒加载指令2.1 使用VueUse2.2 使用IntersectionObserver 前言 图片懒加载是一种常见性能优化的方式&#xff0c;它只去加载可视区域图片&#xff0c;而不是在网页加载完毕后就立即加载所有图片…

安防监控视频云存储平台EasyNVR通道频繁离线的原因排查与解决

安防视频监控汇聚EasyNVR视频集中存储平台&#xff0c;是基于RTSP/Onvif协议的安防视频平台&#xff0c;可支持将接入的视频流进行全平台、全终端分发&#xff0c;分发的视频流包括RTSP、RTMP、HTTP-FLV、WS-FLV、HLS、WebRTC等格式。为了满足用户的集成与二次开发需求&#xf…

H3C交换机如何配置本地端口镜像并在PC上使用Wireshake抓包

环境: H3C S6520-26Q-SI version 7.1.070, Release 6326 Win 10 专业版 Wireshake Version 4.0.3 问题描述: H3C交换机如何配置本地端口镜像并在PC上使用Wireshake抓包 解决方案: 配置交换机本地端口镜像 1.进入系统视图,并创建本地镜像组1 <H3C>system-vie…