草庐IT

基于SpringBoot实现单元测试的多种情境/方法(二)

天翼云开发者社区 2023-04-11 原文

本文分享自天翼云开发者社区@《基于SpringBoot实现单元测试的多种情境/方法(二)》,  作者:才开始学技术的小白

 

1 Mock基础回顾

在上一篇分享中我们详细介绍了简单的、用mock来模拟接口测试环境的方法,具体的使用样例我们再回顾一下:

1.首先是最简单的不需要传参的示例,需要注意的是,可能@Resource这个注解识别不了,没关系,换成@Autowired通常是等效的

//进行每一次mock模拟tomcat容器的时候,使用随机端口启动,这样不会有端口占用的问题@SpringBootTest(classes = {Springboot01Application.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)//自动配置以及启用mvc对象@AutoConfigureMockMvcpublic class MockMVCTester {

    //注入MockMVC对象,它是springtest依赖中自带的

    @Resource

    private MockMvc mockMvc;

    @Test

    public void testMock() throws Exception {

        //获取mock返回的对象

        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/user"))//perform模拟一个http请求,这里是get方法

                .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器返回的是200

                .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出

                .andExpect(MockMvcResultMatchers.content().string("[{\"uid\":1001,\"uname\":\"wu\"," +

                        "\"password\":\"1212\",\"addrs\":[\"nanchang\",\"sichuan\",\"beijing\"]}," +

                        "{\"uid\":1002,\"uname\":\"du\",\"password\":\"1313\",\"addrs\"" +

                        ":[\"chang\",\"sica\",\"beng\"]}]"))//content表示对于返回的请求体数据进行判断,string表示进行比对

                .andReturn();//将结果返回出来

    }}

2.如果get方法需要传参,通常是在query中(也就是问号传参的形式),也有可能是正常传参,而且这种通常有json返回,正常传参的示例如下(.param也可以改为.params);如果是query传参,改用.queryparam即可

@Test@DisplayName("get方法+有入参+有json返回")public void testMock1() throws Exception {

    //mock返回的对象可以不获取,因为单纯的判断对错用不上

    mockMvc.perform(MockMvcRequestBuilders.get("/user/para")//perform模拟一个http请求,这里是get方法

                    .header("token", "akakak")//请求头

                    .param("id","wy")//请求参数

                    .param("password","asd"))//请求参数

            .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器回的是200

            .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出

            .andExpect(MockMvcResultMatchers.jsonPath("ak").value("asd"))//获取返回的json并核对对应的值是否一样

            .andReturn();//将结果返回出来}

3.最后就是post方法的常用body传参,一般都是json格式,示例如下;这里用到了IoC创建对象,不了解的读者可以看看我专栏的IoC相关分享

@Test@DisplayName("post方法测试用例")public void testMock1() throws Exception {

    //IoC

    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("IoC.xml");

    User user = context.getBean(User.class);

    ObjectMapper mapper = new ObjectMapper();

    user.setUname("wy");

    //mock返回的对象可以不获取,因为单纯的判断对错用不上

    mockMvc.perform(MockMvcRequestBuilders.post("/user")//perform模拟一个http请求,这里是get方法

                    .content(mapper.writeValueAsString(user))//用IoC建立一个User对象

                    .contentType(MediaType.APPLICATION_JSON_VALUE))//添加json类数据,转化为入参

            .andExpect(MockMvcResultMatchers.status().isOk())//添加预期,如果服务器回的是200

            .andDo(MockMvcResultHandlers.print())//那我们就把请求和响应的信息在控制台中打印输出

            .andExpect(MockMvcResultMatchers.jsonPath("uname").value("wy"))//获取返回的json并核对对应的值是否一样

            .andReturn();//将结果返回出来}

2 Mock的进阶用法

当然了,Mock不可能只有这些模拟接口的简单用法,本文就介绍一写些其他的常用模式

2.1 简便创建单元测试

如果你使用的IDE和我一样,是IDEA,那么你可以通过在需要写单元测试的方法上Ctrl+Shift+T来快捷创建

 

之后你当然可以选择是否增加运行在测试之前的@Before、运行在测试之后的@After(即Junit的相关注解,在这里不赘述)

注:可以自行查阅JUnit文档:https://junit.org/junit5/docs/current/user-guide/

2.2 @Mock、@Spy、@InjectMocks

有的时候我们需要测试的单元——比如说某一个类,可能依赖于其他的类,而且这个被依赖的类往往不是很好构造,因为他们可能又依赖于其他的类、库、底层资源等等,这个时候mock就帮上了大忙:

1.创建测试用的类

同样我们来举例说明一下,假设我们有一个UserInfo类存储了用户信息,他长这个样子:

public class UserInfo {

    private UserRepository userRepository;

 

    private UserWord userWord;

 

    public String getUserAddr(){

        return userRepository.getUsrAddr();

    }}

非常简单的一个UserInfo类,可以看到他依赖于两个子类,一个是用户数据库(UserRepository),一个是用户词(UserWord)

**都是随便编的哈不要在意**

这两个子类长这个样子:

@Datapublic class UserWord {

    private String word1;

    private String word2;}

@Datapublic class UserRepository {

 

    private String repUUID;

 

    private String usrPassword;

 

    private String usrAddr = "Sichuan";}

也一样非常简单,但大家可以看到我们把UserRepository的usrAddr这个属性赋了一个默认值“Sichuan”,为什么呢?就是为了解释@Spy和@Mock的作用

2.几个重要注解的定义:

@Mock 非真实执行,用来模拟在测试中不好创建的类

@Spy 真实执行,用来模拟需要真实执行的类

@InjectMocks 真实执行,针对实现类使用,不能作用在接口上

大家可能看的一头雾水,但其实用我们刚刚的UserInfo示例来解释,就非常简单:

UserRepository这个类,虽然在我们这里很简单,但我们把他当做一个数据库类来理解——也就是说,在真实开发中,这样的类在单元测试中是不好处理的,因为你不可能为了测试UserInfo这个类的方法,去专门建一个数据库,这个太麻烦了。

那么@Mock就起到了这么一个作用,他相当于给所注解的实例套了一个外壳,我们不用关心里面是怎么样的,系统将其全部设置为null

相当于我们在单元测试的时候,不管@Mock所在的类究竟是什么样子,也不需要专门为了他去建数据库、建依赖。

这就是Mock的核心作用——依赖解耦,尤其是在单元测试中,我们要斩断复杂的类依赖关系,专心测试某一块的功能。

那么@Spy就很好理解了,有些我们需要用到其中功能的类,或者说比较简单的类,就用@Spy来注入,@Mock和@Spy都是将实例注入到@InjectMocks所标注的地方,用一段测试代码来展示这一点:

@SpringBootTest(classes = {Springboot01Application.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)public class MockTest2 {

    @Mock

    private UserRepository userRepository;

 

    @Spy

    private UserWord userWord;

 

    @InjectMocks

    private UserInfo userInfo;

 

    @Test

    public void test1(){

        Assertions.assertEquals("Sichuan",userInfo.getUserAddr());

    }}

可以看到,我们并没有专门创建UserInfo的子类,而是直接用了注入的办法;这其实跟Spring本身的@Component+@Autowired的方法有点类似,

这里如果去运行test1,是不会通过的;因为UserRepository使用@Mock来注入的,里面的东西都是null,如果改成@Spy,测试就可以通过了

2.3 桩

有一种情况:UserRepository不好创建所以我们用Mock模拟一个,但万一我还是想要用里面的某个方法呢?万一UserInfo的某个方法依赖于UserRepository的某一个方法的返回呢?这种情况我显然不能再返回null了,所以“桩”就用在了这种情况:

桩函数(stub):使用一些自己定义的测试函数来替换当前需要测试的函数。被替换的函数可能是目前还没写完的,这样能够加速开发,或更好的找错误源。

打桩(存根):模拟要调用的函数(打桩对象),给它提供桩函数,给桩函数返回一个值。简单的说自定义输入输出,不打桩默认返回null

也就是说,我可以让UserRepository去打桩来返回一个我需要的值!!!

这样一来就非常方便了,举一个非常常用且经典的例子:

    @Test

    public void test1(){

        Mockito.when(userRepository.getUsrAddr()).thenReturn("Beijing");

        Assertions.assertEquals("Beijing",userInfo.getUserAddr());

    }

可以看到,新的test1如上,when...thenReturn方法设置了这个Mock对象的方法被调用的时候应该返回一个什么样的值,即我们自定义了Mock方法的出入参(当然了这里没有入参),这个测试案例是可以通过的,因为方法被调用的时候不再返回默认值null了

when...thenReturn就是我们的桩函数,

Mockito.when(userRepository.getUsrAddr()).thenReturn("Beijing");就是我们的打桩过程

实际上Mockito库有很多方法可以供我们调用,即使是@Spy注入的类也可以使用,大家可以自行去查手册,或者参考下表:

 

3 Mock也有解决不了的情况

很多开发都会接触到Linux系统,如果有些功能是给Linux写的,集成测试、系统测试就需要去搭建一套测试环境,但怎么去做单元测试呢?单元测试就应该尽可能的简单和全面,搭建一套这个环境也太麻烦了吧

没办法这次还真得搭,Mock也模拟不了虚拟机

我们是可以在IDEA中借用一些xshell来使用bash命令,但真正要跑测试,比较方便的还是自己搭一台虚拟机出来,如果有不熟悉的可以关注我的Linux专栏,有搭建虚拟机的经验分享。

搭建并配网好了之后需要给虚拟机安装Java,这个过程就比较简单了,贴个链接的大家可以参考:https://blog.csdn.net/wcy1900353090/article/details/125121855

安装好了之后就可以按如下流程用IDEA链接虚拟机进行测试:

 

 

 

 

 

 

 

有关基于SpringBoot实现单元测试的多种情境/方法(二)的更多相关文章

  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 - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

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

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

  5. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  6. Ruby 方法() 方法 - 2

    我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby​​-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco

  7. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  8. ruby - 使用 C 扩展开发 ruby​​gem 时,如何使用 Rspec 在本地进行测试? - 2

    我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当

  9. ruby - Highline 询问方法不会使用同一行 - 2

    设置:狂欢ruby1.9.2高线(1.6.13)描述:我已经相当习惯在其他一些项目中使用highline,但已经有几个月没有使用它了。现在,在Ruby1.9.2上全新安装时,它似乎不允许在同一行回答提示。所以以前我会看到类似的东西:require"highline/import"ask"Whatisyourfavoritecolor?"并得到:Whatisyourfavoritecolor?|现在我看到类似的东西:Whatisyourfavoritecolor?|竖线(|)符号是我的终端光标。知道为什么会发生这种变化吗? 最佳答案

  10. ruby - Ruby 的 Hash 在比较键时使用哪种相等性测试? - 2

    我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。

随机推荐