如何从基础使用RSpec来测试用户输入

在我的Microverse旅程中,让我最难理解的一件事是RSpec,它是用于测试驱动开发的Ruby宝石。 这个概念很简单。 您可以在编写代码时创建测试,因此将来,如果更新破坏了某些内容,由于一个或多个测试将失败,因此很容易注意到。

如果您的计算机上未安装RSpec,请按照本指南了解如何获取。 除了安装之外,它还说明了如何将RSpec应用于文件,还提供了有关测试输出的一些详细信息。

第一步很容易遵循,但是出现了一个挑战:测试用户输入。 由于我不想失去刚刚学到的知识,并且在此问题上找不到指导,因此我想让自己和其他人的生活更加轻松,因此,我以后可以将其用作参考,或者至少可以将其用作参考。 ,快速提醒。

范围

RSpec的语法使我们可以创建各种范围,可用于通过测试共享变量。

RSpec.describe Class do
  # Declare variables that are readable by all tests of this class

  describe '#method' do
    # Declare variables that are readable by all tests of this method

    it 'nice description of what this test is testing' do
      # Test especifications
    end

    it 'you can have more than one test per method!' do
      # Different test especifications
    end
  end

  describe '#another_method' do
    # You can describe as many methods as you want
    # inside a class!

    it 'another_methods test' do
	#Especifications
    end
  end
end

RSpec.describe DifferentClass do
  # Create different scopes for each class!
end

注意,在RSpec.describe Class doClass不是字符串。 it方法定义要实施的每个测试。 同样,可以跨范围共享对象和测试。 查看文档以获取有关此功能的更多信息。

变数

RSpec为变量声明提供了let方法。 也将工作,而传统的方式let创建仅在需要时通过测试的变量,从而节省内存和加速计算过程。 您也可以使用let! 如果要在测试执行之前加载变量。

# These two methods of variable declaration are analogous

let( :var ) { Class.new }
var = Class.new

let方法中,变量名必须始终是符号,并且其内容可以是任何原始数据类型或对象。

# It also works for doubles

let( :var ) { double( 'class' ) }
var = double( 'class' )

double()方法创建一个通用对象,您可以将行为归于该对象(即使您也可以对真实对象执行此操作)。 'class'字符串是描述性的,不会将对象链接到任何特定的类。

示例代码

从这一点开始,我们将开始为两个类编写测试: ClassyClassCitizen 。 为了方便起见,我将在同一代码块中同时显示测试的方法和测试,但是在您的工作中,您应该有一个专用于核心程序的文件,以及另一个用于所有测试的文件。

我还创建了一个存储库,用于存储测试示例。 检查一下,随时克隆它!

基本测试结构

在此示例中,我们要测试Citizen类的sum()方法。

#========== Class ==========

class Citizen
  attr_accessor :name , :job
  def initialize (name, job)
    @name = name
    @job = job
  end

  def sum (num1, num2)
    output = num1 + num2
    output
  end
end

#========== Test ==========

RSpec.describe Citizen do
  describe '#sum' do
    let( :accountant ) { Citizen.new( 'James' , 'Accountant' ) }

    it 'returns the sum of two numbers' do
      expect(accountant.sum( 1 , 2 )).to eq( 3 )
    end
  end
end

测试的第一步是创建要测试的对象的实例。 之后,您可以使用Expect expect()方法创建一个Mock 。 模拟是测试成功或失败的要求。

在这种情况下,我们模拟了account.sum()方法在接收到12作为输入并期望其返回等于3的值eq(3)由匹配器eq(3)表示eq(3)

RSpec提供了各种各样的匹配器,甚至提供了创建自定义匹配器的可能性。 检查匹配器完整列表,以找到最适合您要测试的匹配器

测试控制台输出

RSpec使您可以测试程序将输出到控制台的消息。 模拟过程与之类似,但是附加了更多元素到expect()方法。 检查下面。

#========== Class ==========

class ClassyClass
  def exists?
    puts 'Yes'
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let( :stacey_instance ) { described_class.new }
  describe '#exists?' do
    it 'outputs a confirmation that it exists' do
      expect { stacey_instance.exists? }.to output( "Yes\n" ).to_stdout
    end
  end
end

ClassyClass ,我们定义的函数exists? 调用时在控制台上输出Yes 。 用于简单测试的简单案例。

该过程与之前相同,但是在这里我们需要使用output()匹配器来编写预期的输出,并且需要.to_stdout来指定内容将显示在控制台中。

在我们的测试对象上,我们使用了内置的described_class.new方法来创建ClassyClass的新实例。 这是RSpec提供的功能,但如果愿意,可以始终使用ClassyClass.new来达到相同的效果。

由于我们正在测试puts方法,因此我们需要在期望中包括\n ,这表示Ruby上的换行符。 如果我们正在测试print方法,我们可以取出\n

我们还可以使用相同的配置测试多行输出。 我们只需要在每行之间添加\就可以了!

#========== Class ==========

class Citizen
  attr_accessor :name , :job
  def initialize (name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def check_citizen (citizen_instance)
    puts "This is #{citizen_instance.name} and he works as #{citizen_instance.job} "
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let( :stacey_instance ) { described_class.new }
  let( :real_deal ) { Citizen.new( 'John' , 'Software Developer' ) }
  let( :double_trouble ) { double( 'citizen' ) }

  describe '#check_citizen' do
    it "tells double_trouble's specific name and job" do
      allow(double_trouble).to receive( :name ).and_return( 'John' )
      allow(double_trouble).to receive( :job ).and_return( 'Software Developer' )

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end

    it "tells real_deal's specific name and job" do
      expect { stacey_instance.check_citizen(real_deal) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end

    it "tells double_trouble's specific name and job using expect" do
      expect(double_trouble).to receive( :name ).and_return( 'John' )
      expect(double_trouble).to receive( :job ).and_return( 'Software Developer' )

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end
  end
end

双打,存根和Mo子

免责声明:这两个术语之间的区别非常微妙,在我的研究中,我注意到它们之间存在重叠。 我会写下我的印象以及理解它们对我有意义的东西,但请记住我是学生,我尚无权限或能力来严格定义这些术语。 为了更好地理解,请检查我在本文末尾留下的参考。

如前所述,双精度对象是通用对象。 它们自己不执行任何操作,您需要通过存根或模拟为它们分配响应。 当您仍然不确定对象的类时,它们很有用。

我对存根的最常见定义是它们是具有固定响应的对象。 我更喜欢将存根视为罐头响应,并将这些对象修改为称为存根对象。 您可以使用allow()方法对对象进行存根。

#========== Class ==========

class Citizen
  attr_accessor :name , :job
  def initialize (name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def check_citizen (citizen_instance)
    puts "This is #{citizen_instance.name} and he works as #{citizen_instance.job} "
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let( :stacey_instance ) { described_class.new }
  let( :real_deal ) { Citizen.new( 'John' , 'Software Developer' ) }
  let( :double_trouble ) { double( 'citizen' ) }

  describe '#check_citizen' do
    it "tells double_trouble's specific name and job" do
      allow(double_trouble).to receive( :name ).and_return( 'John' )
      allow(double_trouble).to receive( :job ).and_return( 'Software Developer' )

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end

    it "tells real_deal's specific name and job" do
      expect { stacey_instance.check_citizen(real_deal) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end
  end
end

在第一个测试中,您可以看到allow()方法的作用。 首先,指定要存根的变量(在这种情况下为double_trouble )。 然后,您可以使用receive()方法确定该对象应该使用哪种方法,并使用and_return()确定调用该方法时它将如何响应。

简而言之,我们可以说

allow(double_trouble).to receive(:name).and_return ('John')

是一种确保double_trouble表现为double_trouble.name = 'John'

请记住, double_trouble不是Citizen的实例。 存根或嘲笑使我们能够将所需的任何行为赋予任何对象,这在没有指定类但知道方法应如何对待对象的情况下很有用。 我们装备了一个双重物体,因此它可以充当真实物体。

第二项测试证实了这一点。 对象real_dealCitizen的实例,具有与存根对象double_trouble相同的属性。 两种方案的输出都相同,并且测试通过。

我们还可以采用更通用的方法,而不为.name.job赋予任何特定的值。

#========== Class ==========

class Citizen
  attr_accessor :name , :job
  def initialize (name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def check_citizen (citizen_instance)
    puts "This is #{citizen_instance.name} and he works as #{citizen_instance.job} "
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let( :stacey_instance ) { described_class.new }
  let( :double_trouble ) { double( 'citizen' ) }

  describe '#check_citizen' do
    it "tells double_trouble's generic name and job" do
      allow(double_trouble).to receive( :name )
      allow(double_trouble).to receive( :job )

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is #{double_trouble.name} and he works as #{double_trouble.job} \n"
      ).to_stdout
    end
  end
end

我们只是放弃了对.name.job响应的能力,并且在输出中,我们希望它能够调用#{double_trouble.name}#{double_trouble.job}

这意味着这两个属性的值是什么都没有关系。 check_citizen()方法将弄清楚如何处理它们,并将相应地产生输出。

够了,现在让我们谈一下模拟。

#========== Class ==========

class Citizen
  attr_accessor :name , :job
  def initialize (name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def check_citizen (citizen_instance)
    puts "This is #{citizen_instance.name} and he works as #{citizen_instance.job} "
  end
end

#========== Test ===========

RSpec.describe ClassyClass do
  let( :stacey_instance ) { described_class.new }
  let( :double_trouble ) { double( 'citizen' ) }

  describe '#check_citizen' do
        it "tells double_trouble's specific name and job using expect" do
      expect(double_trouble).to receive( :name ).and_return( 'John' )
      expect(double_trouble).to receive( :job ).and_return( 'Software Developer' )

      expect { stacey_instance.check_citizen(double_trouble) }.to output(
        "This is John and he works as Software Developer\n"
      ).to_stdout
    end
  end
end

使用嘲笑的测试与使用存根测试的测试几乎相同,不同之处在于使用expect()而不是allow() 。 这是可能的,因为在每种情况下,只有最后的expect()被视为不确定的期望。 其他被视为理所当然的期望。

从概念上讲,它们也不相同。 对对象进行存根时,您将对其执行操作,而在进行模拟时,您假定它们已经可以执行这些操作。

这是一个非常细微的差异,以我作为初学者的经验,我仍然没有发现这些技术的结果存在实际差异的情况。

也许在更复杂的场景中,应用一个或另一个非常有用。 现在,我更喜欢使用allow()因为一眼就能看出哪些行正在执行动作,哪一行是期望值。

用户输入

模拟用户输入并不难,但是可以诱使初学者花费数小时的研究时间,尤其是如果他/她像我一样固执己见,并希望获得最有效的解决方案。

好吧,我提出的解决方案是非常普通的(这意味着您不需要安装其他gems),但是它确实需要我们要求在ruby会话中默认未加载的类。 因此,对我来说是裸露的,不要介意这种香草方式的“过犯”,当您意识到自己将拥有另一种工具时,它会有所回报。

我正在谈论的类称为StringIO 。 它创建存储一个或多个字符串的对象。 当与变量$stdin关联时,每当代码要求用户输入时,此对象将提供其存储的内容。

让我们在IRB上玩这个对象,让您习惯它! 在您的终端上致电irb ,然后按照以下说明进行操作。

require 'stringio'

io = StringIO.new( 'Hi!' )
# The string is optional. You can create an empty StringIO 
# if you want.

io.puts 'Hello!'
# This is another way of adding strings to the object

$stdin = io
# Plug the object into $stdin so it uses io inputs instead of asking 
# the user for it

gets
gets
gets
# Call gets two times, and you'll receive 'Hi!' and 'Hello!', from 
# the third time on, you'll get nil.

io.rewind
# Sets the pointer of the object to the first string

gets
# Calling gets returns 'Hi!' again!

$stdin = STDIN
# After experimenting, set $stdin back to its original.

如果您对STDIN感到好奇,可以查看本文 ,但基本上,这是一个常量,可以“监听”用户输入,并将其通过$stdin变量传递给ruby程序。

现在您已经对StringIO对象有了很好的了解,让我们继续进行测试。

#========== Class ==========

class Citizen
  attr_accessor :name , :job
  def initialize (name, job)
    @name = name
    @job = job
  end
end

class ClassyClass
  def change_name (citizen_instance)
    puts "What's the new name of the citizen?"
    citizen_instance.name = gets.chomp
  end
end

#========== Test ===========

require 'stringio'

RSpec.describe ClassyClass do
  let( :stacey_instance ) { described_class.new }

  describe '#change_name' do
    let( :input ) { StringIO.new( 'Larry' ) }
    let( :new_deal ) { Citizen.new( 'Mark' , 'Banker' ) }

    it "receive user input and change citizen's name" do
      $stdin = input
      expect { stacey_instance.change_name(new_deal) }.to output(
        "What's the new name of the citizen?\n"
      ).to_stdout. and change { new_deal.name }.to( 'Larry' )

      $stdin = STDIN
    end
  end
end

我的目标是测试方法change_name()使用用户提供的字符串更改Citizen对象的name属性的能力。

第一步是使用要作为用户输入传递的字符串创建StringIO对象,并将其插入$stdin 。 接下来,您可以使用expect()方法定义期望。

最初,我仅凭一个期望就创建了该测试,但是问题总是出现在RSpec输出中。 解决方案是创建另一个期望,以测试change_name()方法生成的输出。

通过.and方法添加了第二个期望值。 然后,我用change()来测试,如果change_name()是能够改变的name的属性new_deal目的是通过提供的字符串input

之后,我将$stdin恢复为其默认配置。 测试有效! 这就是测试用户输入的方式!

结论

RSpec比我在本文中展示的方式提供了更多的可能性,但是我相信,按照我给出的示例并牢记所有这些概念,初学者可以立即开始下注。

参考文献

From: https://hackernoon.com/how-to-use-rspec-from-basics-to-testing-user-input-i03k36m3

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值