相关文章推荐
失眠的玉米  ·  Java开发最佳实践(二) ...·  13 小时前    · 
坚韧的小刀  ·  传古名剑再现江湖 龙泉铸剑师有个“侠客梦”-新华网·  4 月前    · 
淡定的可乐  ·  武汉大学新闻与传播学院与韩国成均馆大学新闻放 ...·  8 月前    · 
胡子拉碴的八宝粥  ·  在 Ubuntu 使用 Google ...·  1 年前    · 
豪气的大葱  ·  超武特工头像 - 快看漫画·  2 年前    · 
彷徨的充电器  ·  琼中举办郑州招商推介会 240余家优强企业参加·  2 年前    · 
Code  ›  如何写好单元测试:Mock脱离数据库+不使用@SpringBootTest「建议收藏」开发者社区
junit单元测试 返回值 mock 单元测试
https://cloud.tencent.com/developer/article/2072372
帅气的闹钟
5 月前
全栈程序员站长

如何写好单元测试:Mock脱离数据库+不使用@SpringBootTest「建议收藏」

前往小程序,Get 更优 阅读体验!
立即前往
腾讯云
开发者社区
文档 建议反馈 控制台
首页
学习
活动
专区
圈层
工具
MCP广场
文章/答案/技术大牛
发布
首页
学习
活动
专区
圈层
工具
MCP广场
返回腾讯云官网
全栈程序员站长
首页
学习
活动
专区
圈层
工具
MCP广场
返回腾讯云官网
社区首页 > 专栏 > 如何写好单元测试:Mock脱离数据库+不使用@SpringBootTest「建议收藏」

如何写好单元测试:Mock脱离数据库+不使用@SpringBootTest「建议收藏」

作者头像
全栈程序员站长
发布 于 2022-08-14 16:38:27
发布 于 2022-08-14 16:38:27
3.6K 0 0
代码可运行
举报
文章被收录于专栏: 全栈程序员必看 全栈程序员必看
运行总次数: 0
代码可运行

大家好,又见面了,我是你们的朋友全栈君。

2022年03月25日更新:觉得没必要Mock的人,估计是没做过多个团队合作的项目,没经历过服务间的调用的。没关系,人总是会长大的。如果你以后接触到了,会感谢现在看到这文章的你。

注意:如果下述内容有说连 数据 库的单元 测试 错误,那就是我的错。因为多年不做单机项目了,都是多服务,UT都是mock的。

如果你有不同意见,不要怀疑,你是对的,我是错的。

补充:当代码里有new 对象的时候PowerMockito.whenNew(entityDao.class).withAnyArguments().thenReturn(entity);

void方法可以使用donothing

目录

1、一般的单元测试写法

2、单元测试步骤

3、对一般的单元测试写法分析优化

4、最佳的单元测试写法:Mock脱离数据库+不启动Spring+优化测试速度+不引入项目组件

一、普遍的单元测试方法

作为一个Java后端程序员,肯定需要写单元测试。我先提供一个典型的单元测试例子:

代码语言: javascript
代码 运行次数: 0
运行
复制
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
@Transactional
@Rollback(true) // 事务自动回滚,默认是true。可以不写
public class HelloServiceTest {
    @Autowired
    private HelloService helloService;
    @Test
    public void sayHello() {
        helloService.sayHello("zhangsan");
    }

这个例子错误点有4个:(本文的错误统一指不标准,实际上这样子写单元测试也可以,只是不规范,显示不出在座各位优秀的编程能力)

1、@Autowired启动了Spring

2、@SpringBootTest启动了SpringBoot环境,而classes = Application.class启动了整个项目

3、通过@Transactional可以知道调用了数据库

4、没有Assert断言

二、一般的错误的单元测试步骤(SpringBoot环境下)

1、使用 @RunWith(SpringRunner.class) 声明在Spring的环境中进行单元测试,这样Spring的相关注解就会被识别并起效

2、然后使用 @SpringBootTest ,它会扫描应用程序的spring配置,并构建完整的Spring Context。

3、通过@SpringBootTest我们可以指定启动类,或者给@SpringBootTest的参数 webEnvironment 赋值为SpringBootTest.WebEnvironment.RANDOM_PORT,这样就会 启动web容器,并监听一个随机的端口 ,同时,为我们 自动装配一个TestRestTemplate类型的bean 来辅助我们发送测试请求。

如果项目稍微复杂一点,像SpringCloud那样多模块,还使用了缓存、分片、微服务、集群分布式等东西,然后电脑配置再差一点,那你每执行一次单元测试的启动-运行-测试时间,漫长得够你去喝杯茶再回来了。

或者你的项目使用了@Component注解(在SpringBoot项目启动的时候就会跟着实例化/启动)

启动类上也定义了启动时就实例化的类

这个@Component注解的类里有多线程方法,随着启动类中定义的ApplicationStartup类启动了,那么 在你执行单元测试的时候,由于多线程任务的影响,就可能对你的数据库造成了数据修改,即使你使用了事务回滚注解@Transactional 。我出现的问题是:在我运行单元测试的时候,代码里的其他类的多线程中不停接收activeMQ消息,然后更新数据库中对应的数据。跟单元测试的执行过程交叉重叠,导致单元测试失败。其他组员在操作数据库的时候,也因为我无意中带起的多线程更改了数据库,造成了开发上的困难。

另外附带@Component源码,顺便学习一下

代码语言: javascript
代码 运行次数: 0
运行
复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    //这个值可能作为逻辑组件(即类)的名称,在自动扫描的时候转化为spring bean,
    //即相当<bean id="" class="" />中的id
    String value() default "";
}

@Component是一个元注解,意思是可以注解其他类注解,如@Controller @Service @Repository @Aspect。官方的原话是:带此注解的类看为组件,当使用基于该注解的配置和类路径扫描的时候,这些类就会被实例化。其他类级别的注解也可以被认定为是一种特殊类型的组件,比如@Repository @Aspect。所以,@Component可以注解其他类注解。

三、优化单元测试写法

我先来上图,这样子写单元测试运行一次所需要的时间。然后我们通过对比,得出编写最佳单元测试的方法。我这个6年前的笔记本,运行一次单元测试,需要差不多1分钟,而经过代码优化,只需要几秒钟。下面是优化方式:

首先,我们要明确单元测试的终极目标,就是 完全脱离数据库 ! 完全脱离数据库 ! 完全脱离数据库 !其次,单元测试是只针对某一个类的一个方法(一个小的单元)来测,在测试过程中,我们不要启动其它东西,要脱离项目中其它因素可能产生的干扰。

所以可以发现上面的例子简直是侮辱了单元测试,最初级的入门的学生才这样写。众所周知,现在看到这里的各位都是架构师的能力,接下来我们一行行代码,一秒五喷,严厉抨击这段错误的单元测试:

1、不应使用 @Autowired

代码语言: javascript
代码 运行次数: 0
运行
复制
@Autowired
private HelloService helloService;

这个@Autowired简直是画蛇添足!就是这个东西启动了Spring。以前没有@Autowired的时候,我们需要这样配置bean属性

代码语言: javascript
代码 运行次数: 0
运行
复制
<property name="属性名" value=" 属性值"/>

这种方式代码较多,配置繁琐,于是Spring 2.5 引入了 @Autowired 注释。

@Autowired的原理

在启动spring IOC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IOC容器自动查找需要的bean,并装配给该对象的属性

代码语言: javascript
代码 运行次数: 0
运行
复制
<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/> 

注意事项:

1、 在使用@Autowired时,会先在IOC容器中查询要自动引入的对应类型的bean

2、如果查询结果刚好为一个,就将该bean装配给@Autowired指定的属性值

3、如果查询的结果不止一个,那么@Autowired会根据属性名来查找。

4、如果查询的结果为空,那么会抛出异常。解决方法:使用required=false

那么问题就来了,我们只是要写单元测试,为什么要启动Spring呢?首先,启动Spring只会让你run->Junit Test的时候程序变慢,这是每次运行单元测试都很慢的原因之一。然后单元测试是只针对某一个类的方法来测,启动Spring完全是多余的,所以我们只需要对应的实体类实例就够了。在需要注入bean的时候,我们直接new,如下

代码语言: javascript
代码 运行次数: 0
运行
复制
@Autowired
private HelloService helloService;
private HelloService helloService = new HelloServiceImpl();
// 这个HelloServiceImpl是你每个接口的对应实现类

2、不应使用 @SpringBootTest

代码语言: javascript
代码 运行次数: 0
运行
复制
@SpringBootTest(classes = Application.class)

这个@SpringBootTest简直犯罪有木有!它就是每次运行单元测试都很慢的罪魁祸首,相信我,把它删掉你的单元测试速度会快的飞起。 @SpringBootTest和@Autowired一样,在单元测试里面是完全多余的 ,根本就不搭边的两个东西!每次单元测试都先启动SpringBoot

然后我们来看一下@SpringBootTest的源码

大概意思:

1、@SpringBootTest是在SpringBoot项目上使用的,它在@SpringBootContextLoader的基础上,配置文件属性的读取。

2、在常规Spring TestContext框架之上提供以下特性:

1)当定义没有特定的@ContextConfiguration(loader=…)时,使用SpringBootContextLoader作为默认的ContextLoader。ContextLoader的作用:实际上由ContextLoaderListener调用执行 根应用上下文 的初始化工作。

2)当不使用嵌套@Configuration时,自动搜索@SpringBootConfiguration,并且没有指定显式的类。

3)允许使用properties属性定义自定义环境属性。

4、为不同的webEnvironment模式提供支持,包括启动一个完全运行的web服务器,监听一个已定义的或随机的端口。

5)注册一个TestRestTemplate或WebTestClient bean,用于在web测试中使用完全运行的web服务器。

使用方式

代码语言: javascript
代码 运行次数: 0
运行
复制
@SpringBootTest(classes = Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
现在一般写成这样
@SpringBootTest(classes = Application.class)
@SpringBootTest
但不管写成怎样,这个注解都不该用

classes = Application.class指定启动类,在执行这里的时候,会读取、解析一些项目配置文件,还会连接数据库,然后如果启动类又带有别的启动类、@Component、多线程等, 在你执行单元测试的时候,程序不止运行慢,时间长,而且由于多线程任务的影响,就可能对你的数据库造成了数据修改,即使你使用了事务回滚注解@Transactional 。

3、不应调用数据库

代码语言: javascript
代码 运行次数: 0
运行
复制
@Transactional
@Rollback(true) // 事务自动回滚,默认是true。可以不写

单元测试的目标,就是 完全脱离数据库 !这个注解如果使用,就是完全背道而驰了,一般使用了这个注解的单元测试,脱离数据库后很多都会执行报错

4、应使用Assert断言

Assert断言的使用方式,可以看这篇博客: 单元测试中Assert断言的使用

那么我们到底应该如何写单元测试呢?

四、正确的单元测试写法:Mock脱离数据库

首先放上正确的单元测试例子

代码语言: javascript
代码 运行次数: 0
运行
复制
    //@SpringBootTest
    //@SpringBootTest(classes = Application.class)
    // 在启动类启动的时候也启动了这个类,所以也要引入进来
    //@Import(ApplicationStartup.class)
    // 不执行项目里Component注解过的方法
    //@TestComponent
    // 注意点一:保留了RunWith注解
    @RunWith(SpringRunner.class)
    public class HelloServiceTest {
        //@Autowired
        // 不使用Autowired,不启动Spring容器,对需要实现的方法实现类直接new进行实例化
        private HelloService helloService = new HelloServiceImpl();
        @Test
        public void sayHello() {
            // 模拟JPA的EntityManager,官方的接口、类都要模拟
            EntityManager em =  init(helloService);
            // any()代替任意类型的参数
            Mockito.doReturn("我是模拟的返回值").when(em).findById( any());
            // 没有返回值的方法,可以不另外写,因为模拟实体类的时候已经自动模拟了
            Mockito.doNothing().when(em).find(any());
            helloService.sayHello("zhangsan");
            Assert.isTrue(true,"完全正确的单元测试");
        EntityManager init(Object classInstance ){
            // 要模拟的类
            EntityManager em = Mockito.mock(EntityManager.class);
            // 指定反射类
            Class<?> clazz = classInstance.getClass();
            // 获得指定类的属性
            Field field = null;
            try {
                field = clazz.getDeclaredField("em");
                // 值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
                // 值为 false 则指示反射的对象应该实施 Java 语言访问检查。
                // 默认 false
                field.setAccessible(true);
                // 更改私有属性的值
                field.set(classInstance, em);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            return em;
    // HelloServiceImpl是实现类,以下代码只是为了表达意思,它的sayHello方法代码为
    class HelloServiceImpl {
        @Autowired
        private EntityManager et;
        sayHello(String name) {
            // 没有返回值的操作数据库的方法
            et.find(name);
            // 有返回值的方法
            String oldSecondName = et.findById(name.substring(2));
    }

可以看到保留了@RunWith注解

1、@RunWith 在JUnit中有很多个Runner,他们负责调用你的测试代码,每一个Runner都有各自的特殊功能,你要根据需要选择不同的Runner来运行你的测试代码。一般都是使用SpringRunner.class

2、如果我们只是简单的做普通Java测试,不涉及Spring Web项目,你可以省略@RunWith注解,这样系统会自动使用默认Runner来运行你的代码。

然后最主要的就是Mock了,Mock所需的jar在这里已经包含

代码语言: javascript
代码 运行次数: 0
运行
复制
        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

到这里你需要一点Mock的基础,Mock就是模拟一切操作数据库的步骤,不执行任何SQL,我们直接模拟这句操作数据库的代码执行时成功的,而且可以模拟任何返回值,主要有两个注解

@MockBean

只要是本地的,自己写的bean,都可以使用这个注解,它会把所有操作数据库的方法模拟。如果是没有返回值的方法,我们就可以不管。如果是有返回值的方法,我们可以给它返回各自我们需要模拟的值。用法如下:

代码语言: javascript
代码 运行次数: 0
运行
复制
             // any()代替任意类型的参数
            Mockito.doReturn("我是模拟的返回值").when(em).findById( any());
            // 没有返回值的方法,可以不另外写,因为模拟实体类的时候已经自动模拟了
            Mockito.doNothing().when(em).find(any());

@SpyBean

如果是我们本地,调用别的公司,别的地方给我们写好的接口,不是操作我们自己的数据库,是我们写好入参,别人给我们返回值,我们就用这个。它的用法和@MockBean一样

二者的主要用法区别:

MockBean 适用本地,模拟全部方法

SpyBean适用远程不同环境, 只模拟个别方法

然后我们这里Mock的是JPA官方的EntityManager,对于官方的接口、类在我们的实现类里面作为private属性来操作数据库,我们可以通过这个方法来模拟

代码语言: javascript
代码 运行次数: 0
运行
复制
    EntityManager init(Object classInstance ){
            // 要模拟的类
            EntityManager em = Mockito.mock(EntityManager.class);
            // 指定反射类
            Class<?> clazz = classInstance.getClass();
            // 获得指定类的属性
            Field field = null;
            try {
                field = clazz.getDeclaredField("em");
                // 值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
                // 值为 false 则指示反射的对象应该实施 Java 语言访问检查。
                // 默认 false
                field.setAccessible(true);
                // 更改私有属性的值
                field.set(classInstance, em);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            return em;
        }

如果你的项目没有这么复杂,你只需要在你想要模拟的类头顶加上这个@MockBean注解就可以了,一般都是用这个,如

代码语言: javascript
代码 运行次数: 0
运行
复制
    public class HelloServiceTest {
        //@Autowired
        // 不使用Autowired,不启动Spring容器,对需要实现的方法实现类直接new进行实例化
        private HelloService helloService = new HelloServiceImpl();
        @MockBean
        HelloDao dao;
        @Test
        public void sayHello() {
            // any()代替任意类型的参数
            Mockito.doReturn("我是模拟的返回值").when(dao).findById( any());
 
推荐文章
失眠的玉米  ·  Java开发最佳实践(二) ——《Java开发手册》之"异常处理、MySQL 数据库" - 行无际
13 小时前
坚韧的小刀  ·  传古名剑再现江湖 龙泉铸剑师有个“侠客梦”-新华网
4 月前
淡定的可乐  ·  武汉大学新闻与传播学院与韩国成均馆大学新闻放送学院2006年学术交流活动通知--武汉大学新闻与传播学院
8 月前
胡子拉碴的八宝粥  ·  在 Ubuntu 使用 Google Test - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天
1 年前
豪气的大葱  ·  超武特工头像 - 快看漫画
2 年前
彷徨的充电器  ·  琼中举办郑州招商推介会 240余家优强企业参加
2 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号