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

如何写好单元测试:Mock脱离数据库+不使用@SpringBootTest「建议收藏」目录1、一般的单元测试写法2、单元测试步骤3、对一般的单元测试写法分析优化4、最佳的单元测试写法:Mock脱离数据库+不启动Spring+优化测试速度+不引入项目组件一、普遍的单元测试方法作为一个Java后端程序员,肯定需要写单元测试。我先提供一个典型的错误的单元测试例子:@RunWith(SpringRunner.class)@SpringBootTest(cla…

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

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

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

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

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

void方法可以使用donothing

目录

1、一般的单元测试写法

2、单元测试步骤

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

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

一、普遍的单元测试方法

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

@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项目启动的时候就会跟着实例化/启动)

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

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

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

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

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

@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

@Autowired
private HelloService helloService;

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

<property name="属性名" value=" 属性值"/>

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

@Autowired的原理

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

<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,如下

@Autowired
private HelloService helloService;

改为:

private HelloService helloService = new HelloServiceImpl();

// 这个HelloServiceImpl是你每个接口的对应实现类

2、不应使用@SpringBootTest

@SpringBootTest(classes = Application.class)

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

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

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

如何写好单元测试:Mock脱离数据库+不使用@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服务器。

使用方式

@SpringBootTest(classes = Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

现在一般写成这样
@SpringBootTest(classes = Application.class)

或者这样
@SpringBootTest

但不管写成怎样,这个注解都不该用

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

3、不应调用数据库

@Transactional
@Rollback(true) // 事务自动回滚,默认是true。可以不写

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

4、应使用Assert断言

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

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

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

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

    //@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在这里已经包含

        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

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

@MockBean

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

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

@SpyBean

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

二者的主要用法区别:

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

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

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

    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注解就可以了,一般都是用这个,如

    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());
            // 没有返回值的方法,可以不另外写,因为模拟实体类的时候已经自动模拟了
            Mockito.doNothing().when(dao).find(any());
            
            helloService.sayHello("zhangsan");
            Assert.isTrue(true,"完全正确的单元测试");
        }

这段代码可能跟上面有点不通,我随手敲的,我要表达的就是:如果你不需要模拟官方的接口、类来操作数据库,那你直接在你的实现类头顶加@MockBean或者@SpyBean注解,然后使用Mockito语法就可以了。

你懂我的意思吧?

部分内容参考:

SpringBoot2.X (十四): @SpringBootTest单元测试_大痴小乙的博客-CSDN博客_springboot2 test

springboot test – 简书

Spring注解之@Component详细解析_L小芸的博客-CSDN博客_@component参数

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/133517.html原文链接:https://javaforall.net

赞 (0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • PS初学者实例教程——图层样式的学习[斜面和浮雕]

    PS初学者实例教程——图层样式的学习[斜面和浮雕]本系列教程由中国教程网互助课堂专为PS新手制作,在这里有系列的教程、练习,并有老师对练习进行点评与指导,欢迎朋友们的光临!本例我们学习“斜面和浮雕”图层样式斜面和浮雕是使用频率最高的一种图层样式,熟悉和掌握操作技巧,我们就可以制作出精彩的效果;特别是等高线,运用得当可以制作出与众不同的特殊视觉效果图象题后留有练习作业题,请选择练习.如果使用了附加方法,请加以说明

    2022年9月4日
  • angularjs技术

    angularjs技术angularjs第一天五大事件1、绑定事件,ng-model;2、绑定对应的控制器,ng-controller;3、绑定对应的APP,ng-app;4、单击事件,ng-click;5、初始化事件,ng-init;引入angularjs,使用angularjs语法&lt;scriptsrc="…/plugins/angularjs/angular.min.js"type=…

    2022年7月25日
  • 使用FastJSON 对Map/JSON/String 进行互转[通俗易懂]

    使用FastJSON 对Map/JSON/String 进行互转[通俗易懂]Fastjson是一个Java语言编写的高性能功能完善的JSON库,由阿里巴巴公司团队开发的主要特性主要体现在以下几个方面:1.高性能fastjson采用独创的算法,将parse的速度提升到极致,超过所有json库,包括曾经号称最快的jackson。并且还超越了google的二进制协议protocolbuf。2.功能强大支持各种JDK类型。包括基本类型、JavaBean、Collection、Ma

    2022年6月20日
  • 上海汉特:金税接口软件的产生背景及功能[通俗易懂]

    上海汉特:金税接口软件的产生背景及功能[通俗易懂]随着互联网的快速发展,在会计电算化及企业信息化的催化之下,国内企业会计人员的工作效率也发生了翻天覆地的变化,早已进入了省时、高效、安全、统一的无缝状态。国内经济结构转型,企业经营效率优先,以及高度竞争造成的高度个性化与迅速改变的客户需求,令企业与顾客、企业与供方的关系变得更加密切和复杂。强化管理,规范业务流程,提高准确度,加快产品销售数据流转,以及为流通领域信息管…

    2022年5月20日
  • Matlab赋值_matlab二维数组赋值

    Matlab赋值_matlab二维数组赋值clear;clc;x=[23457810111415161819];y=[106。42108。26109。58109。5110109。93110。49110。59110。6110。9110。76111111。2];x3=min(x):0。1:max(x);%可以放在循环外%v=zeros(3,6);没有必要,需要的话可以,v=[];forn=1:…

    2022年9月27日
  • 计算机专业的男生喜欢你,男生暗恋你的20个动作 一秒看出他喜欢你

    计算机专业的男生喜欢你,男生暗恋你的20个动作 一秒看出他喜欢你很多女生想知道是不是有男生在暗恋自己,下面小编为大家介绍一下男生暗恋你的20个动作。1、眼睛总是忍不住盯着那个女生。2、在女生的面前会表现得特别活跃。3、在女生开心的时候会很开心,在女生伤心的时候,心情就会像乌云密布的天空。4、如果那个女生和其他男生表现得很要好,就会忍不住要吃醋。5、如果那个女生心情低落,就会想过去安慰她。6、对于女生所拜托的事情,就算再难做也会答应。7、女生一上Q,就会很兴奋,…

    2022年7月25日

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号