rspec 测试页面元素
在我的Microverse旅程中,让我最难理解的一件事是RSpec,它是用于测试驱动开发的Ruby宝石。 这个概念很简单。 您可以在编写代码时创建测试,因此将来,如果更新破坏了某些内容,由于一个或多个测试将失败,因此很容易注意到。
如果您的计算机上未安装RSpec,请按照本指南了解如何获取。 除了安装之外,它还说明了如何将RSpec应用于文件,还提供了有关测试输出的一些详细信息。
第一步很容易遵循,但是出现了一个挑战:测试用户输入。 由于我不想失去刚刚学到的知识,并且在寻找有关此问题的指南时遇到了麻烦,因此我想让自己和其他人的生活更加轻松,因此,我以后可以将其用作参考,或者至少可以将其用作参考。 ,快速提醒。
范围
RSpec的语法使我们能够创建各种范围,可用于通过测试共享变量。
RSpec.describe Classdo
# 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 do
, Class
不是字符串。 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'
字符串是描述性的,不会将对象链接到任何特定的类。
示例代码
从这一点开始,我们将开始为两个类编写测试: ClassyClass
和Citizen
。 为方便起见,我将在同一代码块中同时显示测试的方法和测试,但是在您的工作中,您应该有一个专用于核心程序的文件,以及另一个用于所有测试的文件。
我还创建了一个存储库,用于存储测试示例。 检查一下,随时克隆它!
基本测试结构
在此示例中,我们要测试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()
方法在接收到1
和2
作为输入并期望它返回等于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_deal
是Citizen
的实例,具有与存根对象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比我在本文中展示的方式提供了更多的可能性,但是我相信,按照我给出的示例并牢记所有这些概念,初学者可以立即开始编写测试。
参考资料
- 作曲比赛 ,乔恩·罗
- 复合期望 ,乔恩·罗
- OSpec项目的RSpec简介
- Ruby中的IO ,作者JoëlQuenneville
- 放手! ,作者:乔恩·罗
- Ruby| Rspec | 嘲弄和存根 (Dickk Dyer)
- Ruby中的StringIO:如何工作以及如何使用它 ,作者:RubyGuides
- 测试与Ruby中的标准输入和输出交互的代码(使用RSpec) ,作者Dhairya Punjabi
翻译自: https://hackernoon.com/how-to-use-rspec-from-basics-to-testing-user-input-i03k36m3
rspec 测试页面元素