RSpec – 快速指南

RSpec – 快速指南


RSpec – 简介

RSpec 是 Ruby 编程语言的单元测试框架。RSpec 与传统的 xUnit 框架(如 JUnit)不同,因为 RSpec 是一种行为驱动的开发工具。这意味着,用 RSpec 编写的测试侧重于被测试应用程序的“行为”。RSpec 不强调应用程序如何工作,而是强调它的行为方式,换句话说,应用程序实际做了什么。

RSpec 环境

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

如果您在 Windows 上安装 Ruby,您应该在 – http://www.rubyinstaller.org 上有适用于 Windows 的 Ruby 安装程序

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

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

在 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 测试工具,因此目标是关注应用程序的功能以及它是否遵循规范。在行为驱动的开发中,规范通常用“用户故事”来描述。RSpec 旨在明确目标代码是否正确运行,换句话说,是否遵循规范。

让我们回到我们的 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

描述关键字

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

上下文关键字

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

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

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”

上下文关键字是不是强制性的,但它有助于增加约它所包含的示例的更多细节。

it 关键字

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

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

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

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

期望关键字

预期关键字用来定义的RSpec的“期待”。这是我们检查特定预期条件是否已满足的验证步骤。

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

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

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

The to keyword

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

The eql keyword

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

在我们的 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 元音方法测试各种类型的输入字符串是有意义的。

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

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

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

rspec的命令提供了许多不同的选择,一饱眼福,键入rspec的-help。下表列出了最流行的选项并描述了它们的作用。

Sr.No. 选项/标志和说明
1

-I PATH

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

2

-r, –require PATH

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

3

–fail-fast

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

4

-f, –format FORMATTER

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

5

-o, –out FILE

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

6

-c, –color

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

7

-b, –backtrace

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

8

-w, –warnings

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

9

-P, –pattern PATTERN

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

10

-e, –example STRING

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

11

-t, –tag TAG

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

RSpec – 匹配器

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

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

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

平等/身份匹配器

匹配器来测试对象或值是否相等。

Matcher 描述 例子
eq 当实际 == 预期时通过 期望(实际)。等于预期
eql 当 actual.eql?(expected) 时通过 期望(实际).to eql 预期
be 当实际.相等时通过?(预期) 预期(实际)。预期
equal 当 actual.equal 时也通过?(预期) 期望(实际)。等于预期

例子

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

比较匹配器

用于比较值的匹配器。

Matcher 描述 例子
> 当实际 > 预期时通过 期望(实际)。要> 预期
>= 当实际 >= 预期时通过 期望(实际)。要> = 预期
< 当实际<预期时通过 期望(实际)。< 预期
<= 当实际 <= 预期时通过 期望(实际)。要<= 预期
be_between inclusive 当实际值 <= min 且 >= max 时通过 期望(实际).to be_between(min, max).inclusive
be_between exclusive 当实际值为 < min 和 > max 时通过 期望(实际).to be_between(min, max).exclusive
match 当实际匹配正则表达式时通过 期望(实际)。匹配(/regex/)

例子

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

类/类型匹配器

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

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

例子

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 的匹配器。

Matcher 描述 例子
be true 当实际 == 真时通过 期望(实际)是真的
be false 当实际 == 假时通过 期望(实际)是假的
be_truthy 当 actual 不是 false 或 nil 时通过 期望(实际)。成为_真实
be_falsey 当 actual 为 false 或 nil 时通过 期望(实际).to be_falsey
be_nil 当实际为零时通过 期望(实际).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

错误匹配器

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

Matcher 描述 例子
raise_error(ErrorClass) 当块引发 ErrorClass 类型的错误时通过。 期望 {block}.to raise_error(ErrorClass)
raise_error(“error message”) 当块引发带有“错误消息”消息的错误时通过。 期望 {block}.to raise_error(“错误信息”)
raise_error(ErrorClass, “error message”) 当块引发带有消息“错误消息”的 ErrorClass 类型的错误时通过 期望 {block}.to raise_error(ErrorClass,“error message”)

例子

将以下代码保存到名为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 类,我们该怎么做呢?我们需要一个测试替身。

此外,如果我们有一个行为类似于 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 示例中创建方法存根时,建议您使用新的 allow() 语法,但我们在此处提供了旧样式,以便您在看到它时会认出它。

RSpec – 钩子

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

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

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

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 在 describe 块的范围内在幕后创建了一个特殊的类。这允许您为此类的实例变量分配值,您可以在示例中的 it 块中访问这些变量。这也使得在我们的测试中编写更清晰的代码变得容易。如果每个测试(示例)都需要 SimpleClass 的实例,我们可以将该代码放在 before 钩子中,而不必将其添加到每个示例中。

请注意,“Creating a new instance of the SimpleClass class”行被写入控制台两次,这表明,在每个it 块中调用 hook 之前

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

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 的优势之一是它提供了许多编写测试、清理测试的方法。当您的测试简短而整洁时,更容易关注预期的行为,而不是关注如何编写测试的细节。RSpec Subjects 是另一种快捷方式,允许您编写简单明了的测试。

考虑这个代码 –

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 对象实例化移动到 describe 行来做到这一点。

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 示例需要一种简单的方法来共享可重用代码。完成此任务的最佳方法是使用 Helpers。帮助程序基本上是您在示例中共享的常规 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 中,这意味着有关您的describecontextit blocks 的数据

让我们看一个例子 –

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 背后的想法之一,测试听起来像英语的行为描述。

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

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

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

现在,让我们试试 doc 格式化程序,运行这个命令 –

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 Formatters 在运行某些测试时如何工作,该测试通常至少调用一次 expect() 方法。

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

RSpec – 期望

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

  • Expectation 只是使用expect()方法it 块中的一个语句就是这样。没有比这更复杂的了。当你有这样的代码时:expect(1 + 1).to eq(2),你的例子中有一个 Expectation 。您期望表达式1 + 1 的计算结果为2措辞很重要,因为 RSpec 是一个 BDD 测试框架。通过将此声明称为期望,很明显您的 RSpec 代码正在描述它正在测试的代码的“行为”。这个想法是你在表达代码应该如何表现,以一种读起来像文档的方式。

  • 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()。

觉得文章有用?

点个广告表达一下你的爱意吧 !😁