草庐IT

Service层代码单元测试以及单元测试如何Mock

程序员杨叔 2023-07-10 原文

一、背景

接着上一篇文章:单元测试入门篇,本篇文章作为单元测试的进阶篇,主要介绍如何对Springboot Service层代码做单元测试,以及单元测试中涉及外调服务时,如何通过Mock完成测试。

二、Springboot Service层代码单元测试

现在项目都流行前后端代码分离,后端使用springboot框架,在service层编写接口代码实现逻辑。假设现在前端不是你写的,你要对你自己写的后端springboot service层提供的接口方法做单元测试,以确保你写的代码是能正常工作的。

Service层代码单元测试:一个简单的service调mapper查询数据库replay_bug表数据量的接口功能

ReplayBugServiceImpl类代码:

@Service
public class ReplayBugServiceImpl implements ReplayBugService {

    @Autowired
    ReplayBugMapper replayBugMapper;

    @Override
    public int queryBugTotalCount() {
        return replayBugMapper.queryBugTotalCount();
    }
}

replayBugMapper.queryBugTotalCount代码:

@Select("select count(1) from replay_bug")
int queryBugTotalCount();

单元测试ReplayBugServiceImplTest类代码:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


@RunWith(SpringRunner.class)
@SpringBootTest
public class ReplayBugServiceImplTest{
    @Autowired
    ReplayBugServiceImpl replayBugService;

    @Test
    public void queryBugTotalCount() {
        int bugCount=replayBugService.queryBugTotalCount();
        System.out.println("结果是:"+bugCount);
    }
}

代码很简单,调用这个接口服务,打印输出,测试是否能正确查出数据。其中关键的两个注解解释:

@RunWith(SpringRunner.class)注解:是一个测试启动器,可以加载SpringBoot测试注解。
让测试在Spring容器环境下执行。如测试类中无此注解,将导致service、dao等自动注入失败。

@SpringBootTest注解:目的是加载ApplicationContext,启动spring容器。

测试结果如下:

更进一步,测试带入参的service接口:新增接口单元测试

import com.test.service.BestTest;
import com.test.domain.UrlWhiteListVO;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ReplayUrlWhiteListServiceImplTest{
    @Autowired
    ReplayUrlWhiteListServiceImpl replayUrlWhiteListService;

    private UrlWhiteListVO urlWhiteListVO;

    @Before
    public void setup(){
        urlWhiteListVO=new UrlWhiteListVO();
        urlWhiteListVO.setAppId(78);
        urlWhiteListVO.setAppName("testAPP");
        urlWhiteListVO.setUrlWhite("http://www.baidu.com");
        urlWhiteListVO.setRemarks("测试一下");
    }

    @Test
    public void save() {
        System.out.println("测试结果:"+replayUrlWhiteListService.save(urlWhiteListVO));
    }
}

比之前多了一个@Before注解,下面自行设置不同的参数值,测试是否在各种入参情况下接口代码都没有问题。

单元测试结果:

数据库检查数据插入成功:

三、单元测试使用Mockito完成Mock测试

实际业务代码中可能会调到其他第三方接口、会和数据库有交互,如果要测试跑通一个场景,准备数据会非常麻烦。而单元测试很多时候,我们只关心自己的代码逻辑是否有漏洞,这个使用就需要用到Mock, 不真实调用,而是将外调的接口、数据库层面都Mock返回自己想要的各类假数据。

因此再进一步,单元测试使用Mockito完成Mock测试:

import com.test.dao.ReplayBugMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;


@RunWith(SpringRunner.class)
@SpringBootTest
public class ReplayBugServiceImplMockTest {

    /**
     * 使用@Autowired是让实例对象正常注入
     * 使用@InjectMocks是为了向里面添加@Mock注入的对象
     * */
    @Autowired
    @InjectMocks
    ReplayBugServiceImpl replayBugService;

    @Mock
    ReplayBugMapper replayBugMapper;
    
    @Before
    public void setup() {
        //添加Mock注解初始化
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void queryBugTotalCount() {
        int count=1;
        Mockito.when(replayBugMapper.queryBugTotalCount()).thenReturn(count);

        int bugCount=replayBugService.queryBugTotalCount();
        System.out.println("Mock单元测试返回的结果是:"+bugCount);
    }
}

同样的接口,之前真实调用数据库的时候,我们看到返回的结果是3。本次Mock测试代码中我们定义了count为1,使用Mockito让数据库调用直接Mock返回我们定义的1,不再真实调用数据库。

测试结果:

Mockito介绍:Mockito是一款用于java开发的mock测试框架,用于快速创建和配置mock对象。通过创建外部依赖的 Mock 对象, 然后将此 Mock 对象注入到测试类中,简化有外部依赖的类的测试。

Mockito使用:在项目pom.xml中引入依赖spring-boot-starter-test,内部就依赖了Mockito

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

测试代码中用到Mockito的注解作用解释:

@InjectMocks:让@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
@Mock:对函数的调用均执行mock,不执行真实调用。

如果只想对某一些外调做mock,其他的外调都走真实调用:

比如Service ReplayServiceImpl中方法如下

public int addBug(ReplayVO replayVO) {
    if(replayManageMapper.addBug(replayVO.getId())==1){
        //判断如果replay_bug表中已经有这条数据,不再重复添加。应对场景是用户多次点击标记记录为待解决bug。
        if(replayBugService.existBugRecords(replayVO)>=1){
            log.info("replay_bug表中数据已存在,不再重复插入数据");
            return 1;
        }else{
            log.info("向replay_bug表中插入数据");
            return replayManageMapper.saveToReplayBug(replayVO.getAppId(),replayVO.getRequestId(),replayVO.getId(),replayVO.getAppName(),replayVO.getSysDomain(),replayVO.getSysUrl(),replayVO.getUserAccount(),replayVO.getParameters(),replayVO.getResponse(),replayVO.getReplayStatus(),CommonUtils.convertDateToTime(replayVO.getReplayTime()));
        }
    }else{
        return 0;
    }
}

第一步先调用replayManageMapper.addBug对replay表中的这条数据更新状态,更新成功后返回1。
第二步再调用replayBugService.existBugRecords判断replay_bug表中是否存在该条记录,如果存在就不再重复插入。
第三步如果不存在,就再调用replayManageMapper.saveToReplayBug,向replay_bug表中插入该条记录。
现在的需求是单元测试时,对第二步外调的其他接口服务replayBugService做Mock处理,对数据库相关的操作不做Mock,真实调用。

单元测试代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ReplayServiceImplTest {

    private ReplayVO replayVO;
    
    @Autowired
    @InjectMocks
    ReplayServiceImpl replayService;

    @Mock
    ReplayBugService replayBugService;

    @Before
    public void setUp() {

        //添加Mock注解初始化
        MockitoAnnotations.initMocks(this);

        replayVO=new ReplayVO();
        replayVO.setAppId(1);
        replayVO.setRequestId(2);
        replayVO.setId(111);
        replayVO.setAppName("testApp");
        replayVO.setSysDomain("www.test.com");
        replayVO.setSysUrl("http://www.test.com/queryList");
        replayVO.setUserAccount("测试人员");
        replayVO.setParameters("{\"userID\":\"123\"}");
        replayVO.setResponse("{\"result\":\"成功\"}");
        replayVO.setReplayStatus(5);
        Date date =new Date();
        replayVO.setReplayTime(date);
    }

    @Test
    public void addBug() {
        Mockito.when(replayBugService.existBugRecords(replayVO)).thenReturn(5);
        System.out.println("返回值:"+replayService.addBug(replayVO));
    }
}

代码解释:

ReplayBugService做Mock处理,所以加了注解@Mock;
ReplayServiceImpl中,由于需要部分外调服务Mock,部分外调服务不Mock,所以需要加上注解@Autowired和@InjectMocks:
使用@Autowired是让实例对象正常注入;
使用@InjectMocks是为了向里面添加@Mock注入的对象;

当replayBugService.existBugRecords(replayVO), Mock返回5,测试结果为:

当replayBugService.existBugRecords(replayVO), Mock返回0,测试结果为:

数据库查看,数据成功插入:

顺带说一下Mockito的@Spy与@Mock区别:

@Spy修饰的外部类,必须是真实存在的,如果没有我们要自己生成创建

Mockito.doReturn(response).when(testService).save(Mockito.any());

@Mock修饰的外部类,是完全模拟出来的,就算项目中没有这个类的实例,也能自己mock出来一个。

比如Spring项目中如果你引入了一个外部的Service:

  • 如果在写单元测试时候,外部的Service能加载到的话就可以使用@Spy注解,因为Spring能为你从外部服务找到这个Service并生成实例注入。
  • 但是如果外部的服务没有部署,那么Spring就不能为你创建实例,就会报错提示你在创建@Spy修饰服务必须要先实例,此时只要用@Mock注解替换@Spy就好了。

最后,如果有很多的类都需要做单元测试,每一个单元测试类的头上都加公共的注解:
@RunWith(SpringRunner.class)
@SpringBootTest
就显得比较麻烦,可以抽出来写成一个Base类,如果Springboot项目有一些公共的配置需要添加也可以放在这个Base类中:

import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class BeseTest {

    @BeforeClass
    public static void init(){
        System.setProperty("server.domain", "test.server.com");
    }
}

然后其他单元测试类使用时继承这个BaseTest类就OK了,不用再每个类都去加公共的注解、配置:

public class ReplayServiceImplTest extends BestTest

================================================================================================
以上就是本次的全部内容,都看到这里了,如果对你有帮助,麻烦点个赞+收藏+关注,一键三连啦~

欢迎下方扫码关注我的vx公众号:程序员杨叔,各类文章都会第一时间在上面发布,持续分享全栈测试知识干货,你的支持就是作者更新最大的动力~

有关Service层代码单元测试以及单元测试如何Mock的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  4. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  5. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  6. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  8. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  9. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  10. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

随机推荐