RSpec - 快速指南


RSpec - 简介

RSpec 是 Ruby 编程语言的单元测试框架。RSpec 与 JUnit 等传统 xUnit 框架不同,因为 RSpec 是Behave驱动的开发工具。这意味着,用 RSpec 编写的测试重点关注正在测试的应用程序的“Behave”。RSpec 并不强调应用程序如何工作,而是强调它的Behave方式,换句话说,应用程序实际执行的操作。

RSpec 环境

首先,您需要在计算机上安装 Ruby。但是,如果您之前还没有这样做,那么您可以从主 Ruby 网站 - Ruby下载并安装 Ruby 。

如果您要在 Windows 上安装 Ruby,您应该在此处获得适用于 Windows 的 Ruby 安装程序 - http://www.rubyinstaller.org

对于本教程,您只需要文本编辑器,例如记事本和命令行控制台。此处的示例将在 Windows 上使用 cmd.exe。

要运行 cmd.exe,只需单击“开始”菜单并键入“cmd.exe”,然后按 Return 键即可。

在 cmd.exe 窗口的命令提示符处,键入以下命令以查看您正在使用的 Ruby 版本 -

ruby -v

您应该看到以下输出,与此类似 -

ruby 2.2.3p173 (2015-08-18 revision 51636) [x64-mingw32]

本教程中的示例将使用 Ruby 2.2.3,但任何高于 2.0.0 的 Ruby 版本都可以。接下来,我们需要为您的 Ruby 安装安装 RSpec gem。gem 是一个 Ruby 库,您可以在自己的代码中使用它。为了安装 gem,您需要使用gem命令。

现在让我们安装 Rspec gem。返回 cmd.exe 窗口并输入以下内容 -

gem install rspec

您应该有一个已安装的依赖 gem 的列表,这些是 rspec gem 正常运行所需的 gem。在输出的末尾,您应该看到类似这样的内容 -

Done installing documentation for diff-lcs, rspec-support, rspec-mocks,
   rspec-expectations, rspec-core, rspec after 22 seconds 
6 gems installed

如果您的输出看起来不完全相同,请不要担心。此外,如果您使用的是 Mac 或 Linux 计算机,您可能需要使用sudo运行gem install rspec命令,或者使用 HomeBrew 或 RVM 等工具来安装 rspec gem。

Hello World

首先,我们创建一个目录(文件夹)来存储 RSpec 文件。在 cmd.exe 窗口中,输入以下内容 -

cd \

然后输入 -

mkdir rspec_tutorial

最后,输入 -

cd rspec_tutorial

从这里,我们将创建另一个名为spec的目录,通过输入 -

mkdir spec

我们将把 RSpec 文件存储在这个文件夹中。RSpec 文件称为“规格”。如果这让您感到困惑,您可以将规范文件视为测试文件。RSpec 使用术语“spec”,它是“规格”的缩写形式。

由于 RSpec 是一个 BDD 测试工具,因此目标是关注应用程序的功能以及它是否遵循规范。在Behave驱动开发中,规范通常用“用户故事”来描述。RSpec 旨在明确目标代码是否Behave正确,换句话说,是否遵循规范。

让我们回到 Hello World 代码。打开文本编辑器并添加以下代码 -

class HelloWorld

   def say_hello 
      "Hello World!"
   end
   
end

describe HelloWorld do 
   context “When testing the HelloWorld class” do 
      
      it "should say 'Hello World' when we call the say_hello method" do 
         hw = HelloWorld.new 
         message = hw.say_hello 
         expect(message).to eq "Hello World!"
      end
      
   end
end

接下来,将其保存到您上面创建的 spec 文件夹中名为 hello_world_spec.rb 的文件中。现在回到 cmd.exe 窗口,运行以下命令 -

rspec spec spec\hello_world_spec.rb

命令完成后,您应该看到如下所示的输出 -

Finished in 0.002 seconds (files took 0.11101 seconds to load) 
1 example, 0 failures

恭喜,您刚刚创建并运行了第一个 RSpec 单元测试!

在下一节中,我们将继续讨论 RSpec 文件的语法。

RSpec - 基本语法

让我们仔细看看HelloWorld示例的代码。首先,如果还不清楚,我们正在测试HelloWorld类的功能。当然,这是一个非常简单的类,仅包含一个方法say_hello()

这是 RSpec 代码 -

describe HelloWorld do 
   context “When testing the HelloWorld class” do 
      
      it "The say_hello method should return 'Hello World'" do 
         hw = HelloWorld.new 
         message = hw.say_hello 
         expect(message).to eq "Hello World!" 
      end
      
   end 
end

描述关键字

单词“describe”是一个 RSpec 关键字。它用于定义“示例组”。您可以将“示例组”视为测试的集合。描述关键字可以采用类名和/或字符串参数您还需要传递一个块参数来描述,这将包含各个测试,或者如 RSpec 中所称的“示例”。该块只是由 Ruby do/end关键字指定的 Ruby 块。

上下文关键字

context关键字与describe类似。它也可以接受类名和/或字符串参数。您还应该使用带有上下文的块。上下文的想法是它包含某种类型的测试。

例如,您可以指定具有不同上下文的示例组,如下所示 -

context “When passing bad parameters to the foobar() method” 
context “When passing valid parameters to the foobar() method” 
context “When testing corner cases with the foobar() method”

context关键字不是强制性的,但它有助于添加有关其包含的示例的更多详细信息。

it 关键字

it一词是另一个 RSpec 关键字,用于定义“示例”。示例基本上是一个测试或测试用例。同样,与describecontext一样,它接受类名和字符串参数,并且应该与块参数一起使用,用do/end指定。在it的情况下,通常只传递字符串和块参数。字符串参数经常使用“should”一词,旨在描述it 块内应该发生什么特定Behave。换句话说,它描述了示例的预期结果。

请注意我们的 HelloWorld 示例中的it 块-

it "The say_hello method should return 'Hello World'" do

该字符串清楚地表明了当我们在 HelloWorld 类的实例上调用 say hello 时会发生什么。RSpec哲学的这一部分,一个Example不仅仅是一个测试,它也是一个规范(a spec)。换句话说,示例既记录又测试 Ruby 代码的预期Behave。

期望关键字

Expect关键字用于定义 RSpec 中的“期望”。这是一个验证步骤,我们检查是否满足特定的预期条件。

从我们的 HelloWorld 示例中,我们有 -

expect(message).to eql "Hello World!"

期望语句的想法是它们读起来像普通英语。您可以大声说出“期望变量消息等于字符串‘Hello World’”。这个想法是它具有描述性并且易于阅读,即使对于项目经理等非技术利益相关者来说也是如此。

The to keyword

to关键字用作expect语句的一部分。请注意,当您希望 Expectation 为 false 时,您还可以使用not_to关键字来表达相反的意思。您可以看到 to 与一个点一起使用,expect(message).to,因为它实际上只是一个常规的 Ruby 方法。事实上,所有 RSpec 关键字实际上只是 Ruby 方法。

The eql keyword

eql关键字是一个特殊的 RSpec 关键字称为 Matcher。您可以使用匹配器来指定您要测试的条件类型为真(或假)。

在我们的HelloWorld Expect语句中,很明显eql意味着字符串相等。请注意,Ruby 中存在不同类型的相等运算符,因此 RSpec 中对应的匹配器也不同。我们将在后面的部分中探讨许多不同类型的匹配器。

RSpec - 编写规范

在本章中,我们将创建一个新的 Ruby 类,将其保存在自己的文件中,并创建一个单独的规范文件来测试该类。

首先,在我们的新类中,它称为StringAnalyzer。您猜对了,这是一个简单的类,用于分析字符串。我们的类只有一个方法has_vowels? 顾名思义,如果字符串包含元音则返回 true,否则返回 false。这是StringAnalyzer的实现-

class StringAnalyzer 
   def has_vowels?(str) 
      !!(str =~ /[aeio]+/i) 
   end 
end

如果您按照 HelloWorld 部分进行操作,则会创建一个名为 C:\rspec_tutorial\spec 的文件夹。

如果有 hello_world.rb 文件,请将其删除,并将上面的 StringAnalyzer 代码保存到 C:\rspec_tutorial\spec 文件夹中名为 string_analyzer.rb 的文件中。

这是我们用于测试 StringAnalyzer 的规范文件的来源 -

require 'string_analyzer' 

describe StringAnalyzer do 
   context "With valid input" do 
      
      it "should detect when a string contains vowels" do 
         sa = StringAnalyzer.new 
         test_string = 'uuu' 
         expect(sa.has_vowels? test_string).to be true 
      end 
		
      it "should detect when a string doesn't contain vowels" do 
         sa = StringAnalyzer.new 
         test_string = 'bcdfg' 
         expect(sa.has_vowels? test_string).to be false
      end 
      
   end 
end

将其保存在同一规范目录中,并将其命名为 string_analyzer_test.rb。

在 cmd.exe 窗口中,cd 到 C:\rspec_tutorial 文件夹并运行以下命令:dir spec

您应该看到以下内容 -

C:\rspec_tutorial\spec 目录

09/13/2015 08:22 AM  <DIR>    .
09/13/2015 08:22 AM  <DIR>    ..
09/12/2015 11:44 PM                 81 string_analyzer.rb
09/12/2015 11:46 PM              451 string_analyzer_test.rb

现在我们要运行我们的测试,运行这个命令:rspec spec

当您将文件夹的名称传递给rspec时,它会运行该文件夹内的所有规范文件。你应该看到这个结果 -

No examples found.

Finished in 0 seconds (files took 0.068 seconds to load)
0 examples, 0 failures

发生这种情况的原因是,默认情况下,rspec只运行名称以“_spec.rb”结尾的文件。将 string_analyzer_test.rb 重命名为 string_analyzer_spec.rb。您可以通过运行以下命令轻松做到这一点 -

ren spec\string_analyzer_test.rb string_analyzer_spec.rb

现在,再次运行rspec spec,您应该看到如下所示的输出 -

F.
Failures:

   1) StringAnalyzer With valid input should detect when a string contains vowels
      Failure/Error: expect(sa.has_vowels? test_string).to be true 
         expected true
            got false
      # ./spec/string_analyzer_spec.rb:9:in `block (3 levels) in <top (required)>'

Finished in 0.015 seconds (files took 0.12201 seconds to load)
2 examples, 1 failure

Failed examples:
rspec ./spec/string_analyzer_spec.rb:6 # StringAnalyzer With valid 
   input should detect when a string contains vowels
Do you see what just happened? Our spec failed because we have a bug in 
   StringAnalyzer. The bug is simple to fix, open up string_analyzer.rb
   in a text editor and change this line:
!!(str =~ /[aeio]+/i)
to this:
!!(str =~ /[aeiou]+/i)

现在,保存您刚刚在 string_analyizer.rb 中所做的更改并再次运行 rspec spec 命令,您现在应该看到如下所示的输出 -

..
Finished in 0.002 seconds (files took 0.11401 seconds to load)
2 examples, 0 failures

恭喜,您的规范文件中的示例(测试)现已通过。我们修复了正则表达式中包含元音方法的错误,但我们的测试还远未完成。

添加更多使用 has元音方法测试各种类型的输入字符串的示例是有意义的。

下表显示了可以在新示例中添加的一些排列(它阻止)

输入字符串 描述 has_vowels 的预期结果?
'aaa'、'eee'、'iii'、'o' 只有一个元音,没有其他字母。 真的
'abcefg' “至少一个元音和一些辅音” 真的
'mnklp' 只有辅音。 错误的
空字符串(无字母) 错误的
'abcde55345&??' 元音、辅音、数字和标点符号。 真的
'423432%%%^&' 仅限数字和标点符号。 错误的
'艾欧' 仅大写元音。 真的
'艾欧欧阿' 仅大写字母和小元音。 真的
'AbCdEfghI' 大写和小写元音和辅音。 真的
'BCDFG' 仅限大写辅音。 错误的
'' 仅限空白字符。 错误的

由您决定将哪些示例添加到您的规范文件中。有许多条件需要测试,您需要确定哪些条件子集最重要并最好地测试您的代码。

rspec命令提供了许多不同的选项,要查看所有选项,请键入rspec -help。下表列出了最流行的选项并描述了它们的用途。

先生。 选项/标志和描述
1

-I 路径

将 PATH 添加到rspec在查找 Ruby 源文件时使用的加载(必需)路径。

2

-r, --需要路径

添加规范中所需的特定源文件。文件。

3

--快速失败

使用此选项,rspec 将在第一个示例失败后停止运行规范。默认情况下,rspec 运行所有指定的规范文件,无论有多少次失败。

4

-f, --format 格式化程序

此选项允许您指定不同的输出格式。有关输出格式的更多详细信息,请参阅格式化程序部分。

5

-o, --out 文件

此选项指示 rspec 将测试结果写入输出文件 FILE 而不是标准输出。

6

-c,--颜色

在 rspec 的输出中启用颜色。成功的示例结果将以绿色文本显示,失败将以红色文本打印。

7

-b,--回溯

在 rspec 的输出中显示完整的错误回溯。

8

-w, --警告

在 rspec 的输出中显示 Ruby 警告。

9

-P, --pattern 模式

加载并运行与模式 PATTERN 匹配的规范文件。例如,如果您传递 -p “*.rb”,rspec 将运行所有 Ruby 文件,而不仅仅是以“_spec.rb”结尾的文件。

10

-e, --示例字符串

此选项指示 rspec 运行描述中包含文本 STRING 的所有示例。

11

-t, --tag 标签

使用此选项,rspec 将仅运行包含标签 TAG 的示例。请注意,TAG 被指定为 Ruby 符号。有关更多详细信息,请参阅有关 RSpec 标签的部分。

RSpec - 匹配器

如果您还记得我们最初的 Hello World 示例,它包含一行,如下所示 -

expect(message).to eq "Hello World!"

关键字 eql 是一个RSpec “匹配器”。在这里,我们将介绍 RSpec 中其他类型的匹配器。

平等/身份匹配器

用于测试对象或值相等性的匹配器。

匹配器 描述 例子
情商 当实际 == 预期时通过 期望(实际).to eq 期望
情商 当实际.eql?(预期)时通过 预期(实际).to eql预期
当实际.等于?(预期)时通过 预期(实际)。预期
平等的 当实际.等于?(预期)时也通过 期望(实际).等于期望

例子

describe "An example of the equality Matchers" do 

   it "should show how the equality Matchers work" do 
      a = "test string" 
      b = a 
      
      # The following Expectations will all pass 
      expect(a).to eq "test string" 
      expect(a).to eql "test string" 
      expect(a).to be b 
      expect(a).to equal b 
   end
   
end

当执行上述代码时,将产生以下输出。您的计算机上的秒数可能略有不同 -

.
Finished in 0.036 seconds (files took 0.11901 seconds to load)
1 example, 0 failures

比较匹配器

用于与值进行比较的匹配器。

匹配器 描述 例子
> 当实际>预期时通过 期望(实际).to > 预期
>= 当实际 >= 预期时通过 期望(实际).to >= 预期
< 当实际 < 预期时通过 期望(实际).to < 预期
<= 当实际 <= 预期时通过 期望(实际).to <= 预期
包含在内 当实际值 <= min 且 >= max 时通过 期望(实际).to be_ Between(min, max).inclusive
be_ Between 排他性 当实际值 < 最小值且 > 最大值时通过 Expect(实际).to be_ Between(min, max).exclusive
匹配 当实际匹配正则表达式时通过 期望(实际).匹配(/正则表达式/)

例子

describe "An example of the comparison Matchers" do

   it "should show how the comparison Matchers work" do
      a = 1
      b = 2
      c = 3		
      d = 'test string'
      
      # The following Expectations will all pass
      expect(b).to be > a
      expect(a).to be >= a 
      expect(a).to be < b 
      expect(b).to be <= b 
      expect(c).to be_between(1,3).inclusive 
      expect(b).to be_between(1,3).exclusive 
      expect(d).to match /TEST/i 
   end
   
end

当执行上述代码时,将产生以下输出。您的计算机上的秒数可能略有不同 -

. 
Finished in 0.013 seconds (files took 0.11801 seconds to load) 
1 example, 0 failures

类/类型匹配器

用于测试对象类型或类的匹配器。

匹配器 描述 例子
成为实例 当实际是预期类的实例时通过。 期望(实际).to be_instance_of(预期)
是_kind_of 当实际是预期类或其任何父类的实例时通过。 期望(实际).to be_kind_of(预期)
回应 当实际响应指定方法时通过。 期望(实际)。响应(预期)

例子

describe "An example of the type/class Matchers" do
 
   it "should show how the type/class Matchers work" do
      x = 1 
      y = 3.14 
      z = 'test string' 
      
      # The following Expectations will all pass
      expect(x).to be_instance_of Fixnum 
      expect(y).to be_kind_of Numeric 
      expect(z).to respond_to(:length) 
   end
   
end

当执行上述代码时,将产生以下输出。您的计算机上的秒数可能略有不同 -

. 
Finished in 0.002 seconds (files took 0.12201 seconds to load) 
1 example, 0 failures

True/False/Nil 匹配器

用于测试值是否为真、假或零的匹配器。

匹配器 描述 例子
是真实的 当实际 == true 时通过 期望(实际)为真
是假的 当实际 == false 时通过 期望(实际).为假
真实 当实际不为 false 或 nil 时通过 期望(实际).to be_truthy
是假的 当实际为 false 或 nil 时通过 期望(实际).to be_falsey
当实际为零时通过 期望(实际).to be_nil

例子

describe "An example of the true/false/nil Matchers" do
   it "should show how the true/false/nil Matchers work" do
      x = true 
      y = false 
      z = nil 
      a = "test string" 
      
      # The following Expectations will all pass
      expect(x).to be true 
      expect(y).to be false 
      expect(a).to be_truthy 
      expect(z).to be_falsey 
      expect(z).to be_nil 
   end 
end

当执行上述代码时,将产生以下输出。您的计算机上的秒数可能略有不同 -

. 
Finished in 0.003 seconds (files took 0.12301 seconds to load) 
1 example, 0 failures

错误匹配器

当代码块引发错误时,用于测试的匹配器。

匹配器 描述 例子
raise_error(错误类) 当块引发 ErrorClass 类型的错误时通过。 期望 {block}.to raise_error(ErrorClass)
raise_error("错误信息") 当块引发错误并显示“错误消息”消息时通过。 期望 {block}.to raise_error(“错误消息”)
raise_error(ErrorClass, "错误信息") 当块引发 ErrorClass 类型的错误并显示消息“错误消息”时通过 期望 {block}.to raise_error(ErrorClass,“错误消息”)

例子

将以下代码保存到名为error_matcher_spec.rb 的文件中,并使用此命令运行它 - rspec error_matcher_spec.rb

describe "An example of the error Matchers" do 
   it "should show how the error Matchers work" do 
      
      # The following Expectations will all pass 
      expect { 1/0 }.to raise_error(ZeroDivisionError)
      expect { 1/0 }.to raise_error("divided by 0") 
      expect { 1/0 }.to raise_error("divided by 0", ZeroDivisionError) 
   end 
end

当执行上述代码时,将产生以下输出。您的计算机上的秒数可能略有不同 -

. 
Finished in 0.002 seconds (files took 0.12101 seconds to load) 
1 example, 0 failures

RSpec - 测试替身

在本章中,我们将讨论 RSpec Doubles,也称为 RSpec Mocks。Double 是一个可以“替代”另一个对象的对象。您可能想知道这到底意味着什么以及为什么需要它。

假设您正在为一所学校构建一个应用程序,并且您有一个代表学生教室的类和另一个代表学生的类,即您有一个 Classroom 类和一个 Student 类。您需要首先为其中一个类编写代码,所以我们从 Classroom 类开始 -

class ClassRoom 
   def initialize(students) 
      @students = students 
   end 
   
   def list_student_names 
      @students.map(&:name).join(',') 
   end 
end

这是一个简单的类,它有一个方法 list_student_names,该方法返回以逗号分隔的学生姓名字符串。现在,我们想为这个类创建测试,但是如果我们还没有创建 Student 类,我们该怎么做呢?我们需要一个测试替身。

另外,如果我们有一个Behave类似于 Student 对象的“虚拟”类,那么我们的 ClassRoom 测试将不依赖于 Student 类。我们称之为测试隔离。

如果我们的 ClassRoom 测试不依赖于任何其他类,那么当测试失败时,我们可以立即知道我们的 ClassRoom 类中存在错误,而不是其他类中存在错误。请记住,在现实世界中,您可能正在构建一个需要与其他人编写的另一个类进行交互的类。

这就是 RSpec Doubles(模拟)发挥作用的地方。我们的 list_student_names 方法调用其 @students 成员变量中每个 Student 对象的 name 方法。因此,我们需要一个实现 name 方法的 Double。

以下是 ClassRoom 的代码以及 RSpec 示例(测试),但请注意,没有定义 Student 类 -

class ClassRoom 
   def initialize(students) 
      @students = students 
   end
   
   def list_student_names 
      @students.map(&:name).join(',') 
   end 
end

describe ClassRoom do 
   it 'the list_student_names method should work correctly' do 
      student1 = double('student') 
      student2 = double('student') 
      
      allow(student1).to receive(:name) { 'John Smith'} 
      allow(student2).to receive(:name) { 'Jill Smith'} 
      
      cr = ClassRoom.new [student1,student2]
      expect(cr.list_student_names).to eq('John Smith,Jill Smith') 
   end 
end

当执行上述代码时,将产生以下输出。您的计算机上经过的时间可能略有不同 -

. 
Finished in 0.01 seconds (files took 0.11201 seconds to load) 
1 example, 0 failures

正如您所看到的,使用测试替身可以让您测试代码,即使它依赖于未定义或不可用的类。此外,这意味着当测试失败时,您可以立即看出这是因为您的类中存在问题,而不是其他人编写的类。

RSpec - 存根

如果您已经阅读了有关 RSpec Doubles(又名 Mocks)的部分,那么您已经看到了 RSpec Stubs。在 RSpec 中,存根通常称为方法存根,它是一种特殊类型的方法,“代表”现有方法,或者甚至尚不存在的方法。

这是 RSpec Doubles 部分的代码 -

class ClassRoom 
   def initialize(students) 
      @students = students 
   End
   
   def list_student_names 
      @students.map(&:name).join(',') 
   end 
end 

describe ClassRoom do 
   it 'the list_student_names method should work correctly' do 
      student1 = double('student') 
      student2 = double('student') 
      
      allow(student1).to receive(:name) { 'John Smith'}
      allow(student2).to receive(:name) { 'Jill Smith'} 
      
      cr = ClassRoom.new [student1,student2]
      expect(cr.list_student_names).to eq('John Smith,Jill Smith') 
   end 
end

在我们的示例中,allow() 方法提供了我们测试 ClassRoom 类所需的方法存根。在本例中,我们需要一个像 Student 类的实例一样运行的对象,但该类实际上还不存在。我们知道Student类需要提供name()方法,我们使用allow()为name()创建一个方法存根。

需要注意的一件事是,RSpec 的语法多年来发生了一些变化。在旧版本的 RSpec 中,上述方法存根将这样定义 -

student1.stub(:name).and_return('John Smith') 
student2.stub(:name).and_return('Jill Smith')

让我们使用上面的代码并用旧的 RSpec 语法替换两行allow() -

class ClassRoom 
   def initialize(students) 
      @students = students 
   end 
   
   def list_student_names 
      @students.map(&:name).join(',') 
   end 
	
end 

describe ClassRoom do 
   it 'the list_student_names method should work correctly' do 
      student1 = double('student') 
      student2 = double('student')
      
      student1.stub(:name).and_return('John Smith')
      student2.stub(:name).and_return('Jill Smith') 
      
      cr = ClassRoom.new [student1,student2] 
      expect(cr.list_student_names).to eq('John Smith,Jill Smith') 
   end 
end

当您执行上述代码时,您将看到此输出 -

.
Deprecation Warnings:

Using `stub` from rspec-mocks' old `:should` syntax without explicitly 
   enabling the syntax is deprec 

ated. Use the new `:expect` syntax or explicitly enable `:should` instead. 
   Called from C:/rspec_tuto 

rial/spec/double_spec.rb:15:in `block (2 levels) in <top (required)>'.
If you need more of the backtrace for any of these deprecations 
   to identify where to make the necessary changes, you can configure 

`config.raise_errors_for_deprecations!`, and it will turn the 
   deprecation warnings into errors, giving you the full backtrace.

1 deprecation warning total

Finished in 0.002 seconds (files took 0.11401 seconds to load)
1 example, 0 failures

当您需要在 RSpec 示例中创建方法存根时,建议您使用新的 allowed() 语法,但我们在此处提供了旧样式,以便您在看到它时能够识别它。

RSpec - 挂钩

当您编写单元测试时,在测试之前和之后运行设置和拆卸代码通常很方便。设置代码是配置或“设置”测试条件的代码。拆卸代码进行清理,确保后续测试的环境处于一致的状态。

一般来说,您的测试应该彼此独立。当您运行一整套测试并且其中一个测试失败时,您希望确信它失败是因为正在测试的代码存在错误,而不是因为先前的测试使环境处于不一致的状态。

RSpec 中最常用的钩子是 before 和 after 钩子。它们提供了一种定义和运行我们上面讨论的设置和拆卸代码的方法。让我们考虑一下这个示例代码 -

class SimpleClass 
   attr_accessor :message 
   
   def initialize() 
      puts "\nCreating a new instance of the SimpleClass class" 
      @message = 'howdy' 
   end 
   
   def update_message(new_message) 
      @message = new_message 
   end 
end 

describe SimpleClass do 
   before(:each) do 
      @simple_class = SimpleClass.new 
   end 
   
   it 'should have an initial message' do 
      expect(@simple_class).to_not be_nil
      @simple_class.message = 'Something else. . .' 
   end 
   
   it 'should be able to change its message' do
      @simple_class.update_message('a new message')
      expect(@simple_class.message).to_not be 'howdy' 
   end
end

当您运行此代码时,您将得到以下输出 -

Creating a new instance of the SimpleClass class 
. 
Creating a new instance of the SimpleClass class 
. 
Finished in 0.003 seconds (files took 0.11401 seconds to load) 
2 examples, 0 failures

让我们仔细看看发生了什么。before(:each) 方法是我们定义设置代码的地方。当您传递 :each 参数时,您将指示 before 方法在示例组中的每个示例之前运行,即上面代码中的描述块内的两个 it 块。

在 @simple_class = SimpleClass.new 行中,我们创建了 SimpleClass 类的新实例并将其分配给对象的实例变量。您可能想知道什么物体?RSpec 在描述块的范围内在幕后创建一个特殊的类。这允许您为此类的实例变量分配值,您可以在示例中的 it 块中访问这些实例变量。这也使得我们可以轻松地在测试中编写更清晰的代码。如果每个测试(示例)都需要 SimpleClass 的实例,我们可以将该代码放在 before 挂钩中,而不必将其添加到每个示例中。

请注意,“创建 SimpleClass 类的新实例”行被写入控制台两次,这表明在每个 it 块中调用 hook之前

正如我们所提到的,RSpec 还有一个 after 钩子,并且 before 和 after 钩子都可以将: all 作为参数。after 挂钩将在指定目标之后运行。: all 目标意味着该钩子将在所有示例之前/之后运行。这是一个简单的示例,说明了何时调用每个钩子。

describe "Before and after hooks" do 
   before(:each) do 
      puts "Runs before each Example" 
   end 
   
   after(:each) do 
      puts "Runs after each Example" 
   end 
   
   before(:all) do 
      puts "Runs before all Examples" 
   end 
   
   after(:all) do 
      puts "Runs after all Examples"
   end 
   
   it 'is the first Example in this spec file' do 
      puts 'Running the first Example' 
   end 
   
   it 'is the second Example in this spec file' do 
      puts 'Running the second Example' 
   end 
end

当您运行上面的代码时,您将看到以下输出 -

Runs before all Examples 
Runs before each Example 
Running the first Example 
Runs after each Example 
.Runs before each Example 
Running the second Example 
Runs after each Example 
.Runs after all Examples

RSpec - 标签

RSpec 标签提供了一种在规范文件中运行特定测试的简单方法。默认情况下,RSpec 将运行它运行的规范文件中的所有测试,但您可能只需要运行其中的一部分。假设您有一些运行速度非常快的测试,并且您刚刚对应用程序代码进行了更改,并且您只想运行快速测试,此代码将演示如何使用 RSpec 标签来执行此操作。

describe "How to run specific Examples with Tags" do 
   it 'is a slow test', :slow = > true do 
      sleep 10 
      puts 'This test is slow!' 
   end 
   
   it 'is a fast test', :fast = > true do 
      puts 'This test is fast!' 
   end 
end

现在,将上述代码保存在名为 tag_spec.rb 的新文件中。从命令行运行以下命令:rspec --tag Slow tag_spec.rb

你会看到这个输出 -

运行选项:包括 {: Slow = >true}

This test is slow! 
. 
Finished in 10 seconds (files took 0.11601 seconds to load) 
1 example, 0 failures

然后,运行以下命令: rspec --tag fast tag_spec.rb

你会看到这个输出 -

Run options: include {:fast = >true} 
This test is fast! 
. 
Finished in 0.001 seconds (files took 0.11201 seconds to load) 
1 example, 0 failures

正如您所看到的,RSpec 标签使测试子集变得非常容易!

RSpec - 主题

RSpec 的优势之一是它提供了多种编写测试、干净测试的方法。当您的测试简短且整洁时,您可以更轻松地关注预期Behave,而不是关注测试编写方式的细节。RSpec 主题是另一个快捷方式,允许您编写简单直接的测试。

考虑这段代码 -

class Person 
   attr_reader :first_name, :last_name 
   
   def initialize(first_name, last_name) 
      @first_name = first_name 
      @last_name = last_name 
   end 
end 

describe Person do 
   it 'create a new person with a first and last name' do
      person = Person.new 'John', 'Smith'
      
      expect(person).to have_attributes(first_name: 'John') 
      expect(person).to have_attributes(last_name: 'Smith') 
   end 
end

实际上已经很清楚了,但是我们可以使用 RSpec 的主题功能来减少示例中的代码量。我们通过将 person 对象实例化移动到描述行来做到这一点。

class Person 
   attr_reader :first_name, :last_name 
   
   def initialize(first_name, last_name) 
      @first_name = first_name 
      @last_name = last_name 
   end 
	
end 

describe Person.new 'John', 'Smith' do 
   it { is_expected.to have_attributes(first_name: 'John') } 
   it { is_expected.to have_attributes(last_name: 'Smith') }
end

当您运行此代码时,您将看到以下输出 -

.. 
Finished in 0.003 seconds (files took 0.11201 seconds to load) 
2 examples, 0 failures

请注意,第二个代码示例要简单得多。我们在第一个示例中采用了一个it 块,并将其替换为两个it 块,这最终需要更少的代码并且同样清晰。

RSpec - 助手

有时,您的 RSpec 示例需要一种简单的方法来共享可重用代码。实现这一目标的最佳方法是使用助手。帮助程序基本上是您在示例中共享的常规 Ruby 方法。为了说明使用助手的好处,让我们考虑一下这段代码 -

class Dog 
   attr_reader :good_dog, :has_been_walked 
   
   def initialize(good_or_not) 
      @good_dog = good_or_not 
      @has_been_walked = false 
   end 
   
   def walk_dog 
      @has_been_walked = true 
   end 
end 

describe Dog do 
   it 'should be able to create and walk a good dog' do 
      dog = Dog.new(true) 
      dog.walk_dog 
      
      expect(dog.good_dog).to be true
      expect(dog.has_been_walked).to be true 
   end 
   
   it 'should be able to create and walk a bad dog' do 
      dog = Dog.new(false) 
      dog.walk_dog 

      expect(dog.good_dog).to be false
      expect(dog.has_been_walked).to be true 
 
   end 
end

这段代码很清晰,但尽可能减少重复代码总是一个好主意。我们可以使用上面的代码,并使用名为 create_and_walk_dog() 的辅助方法来减少一些重复。

class Dog
   attr_reader :good_dog, :has_been_walked 
   
   def initialize(good_or_not)
      @good_dog = good_or_not 
      @has_been_walked = false 
   end 
   
   def walk_dog 
      @has_been_walked = true 
   end 
end 

describe Dog do 
   def create_and_walk_dog(good_or_bad)
      dog = Dog.new(good_or_bad)
      dog.walk_dog
      return dog 
   end 
   
   it 'should be able to create and walk a good dog' do
      dog = create_and_walk_dog(true)
      
      expect(dog.good_dog).to be true
      expect(dog.has_been_walked).to be true 
   end 
   
   it 'should be able to create and walk a bad dog' do 
      dog = create_and_walk_dog(false)
      
      expect(dog.good_dog).to be false
      expect(dog.has_been_walked).to be true 
   end 
end

当您运行上面的代码时,您将看到以下输出 -

.. 
Finished in 0.002 seconds (files took 0.11401 seconds to load) 
2 examples, 0 failures

正如您所看到的,我们能够将创建和遛狗对象的逻辑推送到 Helper 中,这使得我们的示例变得更短、更清晰。

RSpec - 元数据

RSpec 是一个灵活而强大的工具。RSpec 中的元数据功能也不例外。元数据通常指“关于数据的数据”。在 RSpec 中,这意味着有关您的描述上下文它块的数据。

让我们看一个例子 -

RSpec.describe "An Example Group with a metadata variable", :foo => 17 do 
   context 'and a context with another variable', :bar => 12 do 
      
      it 'can access the metadata variable of the outer Example Group' do |example| 
         expect(example.metadata[:foo]).to eq(17) 
      end
      
      it 'can access the metadata variable in the context block' do |example|  
         expect(example.metadata[:bar]).to eq(12) 
      end 
      
   end 
end

当您运行上面的代码时,您将看到以下输出 -

.. 
Finished in 0.002 seconds (files took 0.11301 seconds to load) 
2 examples, 0 failures

元数据提供了一种在 RSpec 文件中的不同范围内分配变量的方法。example.metadata 变量是一个 Ruby 哈希,其中包含有关示例和示例组的其他信息。

例如,让我们将上面的代码重写为如下所示 -

RSpec.describe "An Example Group with a metadata variable", :foo => 17 do
   context 'and a context with another variable', :bar => 12 do 
      
      it 'can access the metadata variable in the context block' do |example|
         expect(example.metadata[:foo]).to eq(17) 
         expect(example.metadata[:bar]).to eq(12) 
         example.metadata.each do |k,v|
         puts "#{k}: #{v}"
      end
		
   end 
end 

当我们运行此代码时,我们会看到 example.metadata 哈希中的所有值 -

.execution_result: #<RSpec::Core::Example::ExecutionResult:0x00000002befd50>
block: #<Proc:0x00000002bf81a8@C:/rspec_tutorial/spec/metadata_spec.rb:7>
description_args: ["can access the metadata variable in the context block"]
description: can access the metadata variable in the context block
full_description: An Example Group with a metadata variable and a context 
   with another variable can access the metadata variable in the context block
described_class:
file_path: ./metadata_spec.rb
line_number: 7
location: ./metadata_spec.rb:7
absolute_file_path: C:/rspec_tutorial/spec/metadata_spec.rb
rerun_file_path: ./metadata_spec.rb
scoped_id: 1:1:2
foo: 17
bar: 12
example_group:
{:execution_result=>#<RSpec::Core::Example::ExecutionResult:
   0x00000002bfa0e8>, :block=>#<
   Proc:0x00000002bfac00@C:/rspec_tutorial/spec/metadata_spec.rb:2>, 
   :description_args=>["and a context with another variable"], 
	
   :description=>"and a context with another variable", 
   :full_description=>"An Example Group with a metadata variable
   and a context with another variable", :described_class=>nil, 
      :file_path=>"./metadata_spec.rb", 
		
   :line_number=>2, :location=>"./metadata_spec.rb:2", 
      :absolute_file_path=>"C:/rspec_tutorial/spec/metadata_spec.rb",
      :rerun_file_path=>"./metadata_spec.rb", 
		
   :scoped_id=>"1:1", :foo=>17, :parent_example_group=>
      {:execution_result=>#<
      RSpec::Core::Example::ExecutionResult:0x00000002c1f690>, 
      :block=>#<Proc:0x00000002baff70@C:/rspec_tutorial/spec/metadata_spec.rb:1>
      , :description_args=>["An Example Group with a metadata variable"], 
		
   :description=>"An Example Group with a metadata variable", 
   :full_description=>"An Example Group with a metadata variable", 
	:described_class=>nil, :file_path=>"./metadata_spec.rb", 
   :line_number=>1, :location=>"./metadata_spec.rb:1",
   :absolute_file_path=>
	
   "C:/rspec_tutorial/spec/metadata_spec.rb", 
   :rerun_file_path=>"./metadata_spec.rb", 
   :scoped_id=>"1", :foo=>17}, 
   :bar=>12}shared_group_inclusion_backtrace: [] 
	
last_run_status: unknown .
.
Finished in 0.004 seconds (files took 0.11101 seconds to load) 
2 examples, 0 failures

最有可能的是,您不需要使用所有这些元数据,而是查看完整的描述值 -

具有元数据变量和具有另一个变量的上下文的示例组可以访问上下文块中的元数据变量。

这是根据描述块描述 + 其包含的上下文块描述 + it的描述创建的句子。

这里值得注意的是,这三个字符串一起读起来就像一个普通的英语句子。。。这是 RSpec 背后的想法之一,让测试听起来像英语的Behave描述。

RSpec - 过滤

在阅读本节之前,您可能需要阅读有关 RSpec 元数据的部分,因为事实证明,RSpec 过滤是基于 RSpec 元数据的。

想象一下,您有一个规范文件,它包含两种类型的测试(示例):正面功能测试和负面(错误)测试。让我们这样定义它们 -

RSpec.describe "An Example Group with positive and negative Examples" do 
   context 'when testing Ruby\'s build-in math library' do
      
      it 'can do normal numeric operations' do 
         expect(1 + 1).to eq(2) 
      end 
      
      it 'generates an error when expected' do
         expect{1/0}.to raise_error(ZeroDivisionError) 
      end
      
   end 
end

现在,将上述文本保存为名为“filter_spec.rb”的文件,然后使用以下命令运行它 -

rspec filter_spec.rb

您将看到类似这样的输出 -

.. 
Finished in 0.003 seconds (files took 0.11201 seconds to load) 
2 examples, 0 failures

现在,如果我们只想重新运行此文件中的阳性测试怎么办?还是只有阴性测试?我们可以使用 RSpec 过滤器轻松做到这一点。将上面的代码更改为:

RSpec.describe "An Example Group with positive and negative Examples" do 
   context 'when testing Ruby\'s build-in math library' do
      
      it 'can do normal numeric operations', positive: true do 
         expect(1 + 1).to eq(2) 
      end 
      
      it 'generates an error when expected', negative: true do 
         expect{1/0}.to raise_error(ZeroDivisionError) 
      end
      
   end 
end

保存对 filter_spec.rb 的更改并运行这个略有不同的命令 -

rspec --tag positive filter_spec.rb

现在,您将看到如下所示的输出 -

Run options: include {:positive=>true} 
. 
Finished in 0.001 seconds (files took 0.11401 seconds to load) 
1 example, 0 failures

通过指定 --tag Positive,我们告诉 RSpec 仅运行定义了 Positive 元数据变量的示例。我们可以通过运行这样的命令来对负面测试做同样的事情 -

rspec --tag negative filter_spec.rb

请记住,这些只是示例,您可以使用任何您想要的名称指定过滤器。

RSpec 格式化程序

格式化程序允许 RSpec 以不同的方式显示测试的输出。让我们创建一个包含此代码的新 RSpec 文件 -

RSpec.describe "A spec file to demonstrate how RSpec Formatters work" do 
   context 'when running some tests' do 
      
      it 'the test usually calls the expect() method at least once' do 
         expect(1 + 1).to eq(2) 
      end
      
   end 
end

现在,将其保存到名为 formatter_spec.rb 的文件并运行此 RSpec 命令 -

rspec formatter_spec.rb

您应该看到如下所示的输出 -

. 
Finished in 0.002 seconds (files took 0.11401 seconds to load) 
1 example, 0 failures

现在运行相同的命令,但这次指定一个格式化程序,如下所示 -

rspec --format progress formatter_spec.rb

这次你应该看到相同的输出 -

. 
Finished in 0.002 seconds (files took 0.11401 seconds to load) 
1 example, 0 failures

原因是“progress”格式化程序是默认格式化程序。接下来让我们尝试不同的格式化程序,尝试运行此命令 -

rspec --format doc formatter_spec.rb

现在你应该看到这个输出 -

A spec file to demonstrate how RSpec Formatters work 
   when running some tests 
      the test usually calls the expect() method at least once
Finished in 0.002 seconds (files took 0.11401 seconds to load) 
1 example, 0 failures

正如您所看到的,“doc”格式化程序的输出完全不同。该格式化程序以类似文档的风格呈现输出。您可能想知道当测试失败时这些选项是什么样的(示例)。让我们将formatter_spec.rb中的代码更改为如下所示 -

RSpec.describe "A spec file to demonstrate how RSpec Formatters work" do 
   context 'when running some tests' do 
      
      it 'the test usually calls the expect() method at least once' do 
         expect(1 + 1).to eq(1) 
      end
      
   end 
end

期望expect(1 + 1).to eq(1)应该失败。保存您的更改并重新运行上述命令 -

rspec --format Progress formatter_spec.rb并记住,由于“progress”格式化程序是默认的,因此您可以运行:rspec formatter_spec.rb。你应该看到这个输出 -

F 
Failures:
1) A spec file to demonstrate how RSpec Formatters work when running some tests 
the test usually calls the expect() method at least once
   Failure/Error: expect(1 + 1).to eq(1)
	
      expected: 1
         got: 2
			  
      (compared using ==)			  
   # ./formatter_spec.rb:4:in `block (3 levels) in <top (required)>'

Finished in 0.016 seconds (files took 0.11201 seconds to load)
1 example, 1 failure
Failed examples:

rspec ./formatter_spec.rb:3 # A spec file to demonstrate how RSpec 
   Formatters work when running some tests the test usually calls 
   the expect() method at least once

现在,让我们尝试一下文档格式化程序,运行以下命令 -

rspec --format doc formatter_spec.rb

现在,由于测试失败,您应该看到以下输出 -

A spec file to demonstrate how RSpec Formatters work
   when running some tests
      the test usually calls the expect() method at least once (FAILED - 1)
		
Failures:

1) A spec file to demonstrate how RSpec Formatters work when running some
   tests the test usually calls the expect() method at least once
   Failure/Error: expect(1 + 1).to eq(1)
	
   expected: 1
        got: 2
		  
   (compared using ==)
   # ./formatter_spec.rb:4:in `block (3 levels) in <top (required)>'
	
Finished in 0.015 seconds (files took 0.11401 seconds to load) 
1 example, 1 failure

失败的例子

rspec ./formatter_spec.rb:3 # 一个规范文件,用于演示 RSpec 格式化程序在运行某些测试时如何工作,测试通常至少调用一次 Expect() 方法。

RSpec 格式化程序提供了更改测试结果显示方式的能力,甚至可以创建您自己的自定义格式化程序,但这是一个更高级的主题。

RSpec - 期望

当您学习 RSpec 时,您可能会阅读很多有关期望的内容,一开始可能会有点困惑。当您看到“期望”一词时,您应该记住两个主要细节 -

  • Expectation 只是it 块中使用expect()方法的语句。就是这样。没有比这更复杂的了。当您有这样的代码时:expect(1 + 1).to eq(2),您的示例中有一个 Expectation 。您期望表达式1 + 1的计算结果为2。不过,措辞很重要,因为 RSpec 是一个 BDD 测试框架。通过将此语句称为“期望”,很明显您的 RSpec 代码正在描述其正在测试的代码的“Behave”。这个想法是,您以类似于文档的方式表达代码的Behave方式。

  • Expectation 语法相对较新。在引入Expect()方法之前(早在 2012 年),RSpec 使用基于should()方法的不同语法。上面的 Expectation 在旧语法中是这样写的:(1 + 1).should eq(2)

在使用基于旧代码或旧版本的 RSpec 时,您可能会遇到期望的旧 RSpec 语法。如果您在新版本的 RSpec 中使用旧语法,您将看到一条警告。

例如,使用以下代码 -

RSpec.describe "An RSpec file that uses the old syntax" do
   it 'you should see a warning when you run this Example' do 
      (1 + 1).should eq(2) 
   end 
end

当你运行它时,你将得到如下所示的输出 -

. Deprecation Warnings:

Using `should` from rspec-expectations' old `:should` 
   syntax without explicitly enabling the syntax is deprecated. 
   Use the new `:expect` syntax or explicitly enable 
	
`:should` with `config.expect_with( :rspec) { |c| c.syntax = :should }`
   instead. Called from C:/rspec_tutorial/spec/old_expectation.rb:3 :in 
   `block (2 levels) in <top (required)>'.

If you need more of the backtrace for any of these deprecations to
   identify where to make the necessary changes, you can configure 
`config.raise_errors_for_deprecations!`, and it will turn the deprecation 
   warnings into errors, giving you the full backtrace.

1 deprecation warning total 
Finished in 0.001 seconds (files took 0.11201 seconds to load) 
1 example, 0 failures

除非您需要使用旧语法,否则强烈建议您使用expect() 而不是should()。