RSpec 快速指南



RSpec - 简介

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

RSpec 环境

首先,您需要在计算机上安装 Ruby。但是,如果您之前还没有安装,则可以从 Ruby 官方网站下载并安装 Ruby - Ruby

如果您在 Windows 上安装 Ruby,则可以在此处找到 Windows 版 Ruby 安装程序 - http://www.rubyinstaller.org

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

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

在 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 使用术语“规范”,它是“规范”的简写形式。

由于 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

接下来,将其保存到名为 hello_world_spec.rb 的文件中,该文件位于您上面创建的 spec 文件夹中。现在返回到您的 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 关键字

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

context 关键字

context 关键字类似于 describe。它也可以接受类名和/或字符串参数。您也应该在 context 中使用块。context 的含义是它包含特定类型的测试。

例如,您可以使用不同的 context 指定示例组,如下所示 -

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 一样,it 接受类名和字符串参数,并且应与块参数一起使用,由 do/end 指定。在 it 的情况下,通常只传递字符串和块参数。字符串参数通常使用“should”一词,旨在描述在 it 块中应该发生什么特定行为。换句话说,它描述了示例的预期结果。

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

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

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

expect 关键字

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

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

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

expect 语句的理念是,它们读起来像正常的英语。您可以大声朗读为“期望变量 message 等于字符串‘Hello World’”。其理念是,它具有描述性并且易于阅读,即使对于非技术利益相关者(如项目经理)也是如此。

The to keyword

to 关键字用作 expect 语句的一部分。请注意,您还可以使用 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

将其保存在同一个 spec 目录中,并将其命名为 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

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

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

下表显示了一些可以在新示例(it 块)中添加的排列。

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

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

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

序号 选项/标志和描述
1

-I PATH

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

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 中的其他类型的匹配器。

相等/同一性匹配器

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

匹配器 描述 示例
eq 当 actual == expected 时通过 expect(actual).to eq expected
eql 当 actual.eql?(expected) 时通过 expect(actual).to eql expected
be 当 actual.equal?(expected) 时通过 expect(actual).to be expected
equal 当 actual.equal?(expected) 时也通过 expect(actual).to equal expected

示例

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

比较匹配器

用于比较两个值的匹配器。

匹配器 描述 示例
> 当 actual > expected 时通过 expect(actual).to be > expected
>= 当 actual >= expected 时通过 expect(actual).to be >= expected
< 当 actual < expected 时通过 expect(actual).to be < expected
<= 当 actual <= expected 时通过 expect(actual).to be <= expected
be_between inclusive 当 actual <= min 且 >= max 时通过 expect(actual).to be_between(min, max).inclusive
be_between exclusive 当 actual < min 且 > max 时通过 expect(actual).to be_between(min, max).exclusive
match 当 actual 与正则表达式匹配时通过 expect(actual).to 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

类/类型匹配器

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

匹配器 描述 示例
be_instance_of 当 actual 是预期类的实例时通过。 expect(actual).to be_instance_of(Expected)
be_kind_of 当 actual 是预期类的实例或其任何父类的实例时通过。 expect(actual).to be_kind_of(Expected)
respond_to 当 actual 响应指定的方法时通过。 expect(actual).to respond_to(expected)

示例

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

真/假/空匹配器

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

匹配器 描述 示例
be true 当 actual == true 时通过 expect(actual).to be true
be false 当 actual == false 时通过 expect(actual).to be false
be_truthy 当 actual 不为 false 或 nil 时通过 expect(actual).to be_truthy
be_falsey 当 actual 为 false 或 nil 时通过 expect(actual).to be_falsey
be_nil 当 actual 为 nil 时通过 expect(actual).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) 当代码块引发类型为 ErrorClass 的错误时通过。 expect {block}.to raise_error(ErrorClass)
raise_error("error message") 当代码块引发消息为“error message”的错误时通过。 expect {block}.to raise_error(“error message”)
raise_error(ErrorClass, "error message") 当代码块引发类型为 ErrorClass 且消息为“error message”的错误时通过 expect {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 类,我们该怎么做呢?我们需要一个测试 Double。

此外,如果我们有一个像 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')

让我们获取上述代码,并将两行allow()替换为旧的 RSpec 语法 -

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 中最常用的钩子是 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 方法在示例组中的每个示例之前运行,即上面代码中 describe 块内的两个 it 块。

在代码行:@simple_class = SimpleClass.new 中,我们正在创建一个 SimpleClass 类的新实例,并将其分配给对象的实例变量。您可能想知道哪个对象?RSpec 在 describe 块的范围内幕后创建一个特殊的类。这允许您将值分配给此类的实例变量,您可以在示例中的 it 块中访问这些变量。这也使得在测试中编写更简洁的代码变得容易。如果每个测试(示例)都需要一个 SimpleClass 的实例,我们可以将该代码放在 before 钩子中,而不必将其添加到每个示例中。

请注意,代码行“正在创建 SimpleClass 类的新实例”被写入了控制台两次,这表明,before 钩子在每个it 块中都被调用了。

正如我们所提到的,RSpec 还有一个 after 钩子,before 和 after 钩子都可以将:all 作为参数。after 钩子将在指定的 target 之后运行。: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

您将看到此输出 -

运行选项:include {: 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 的 subject 功能来减少示例中的代码量。我们可以通过将 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。Helpers 本质上是您在示例之间共享的普通 Ruby 方法。为了说明使用 Helpers 的好处,让我们考虑以下代码 -

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() 的 helper 方法来减少一些重复。

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 中的 Metadata 功能也不例外。Metadata 通常指的是“关于数据的数据”。在 RSpec 中,这意味着关于您的 **describe**、**context** 和 **it 块** 的数据。

让我们来看一个例子 -

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

Metadata 提供了一种在 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

您很可能不需要使用所有这些元数据,但请查看完整描述值 -

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

这句话是由 describe 块描述 + 其包含的 context 块描述 + **it 块**的描述组成的。

这里有趣的一点是,这三个字符串连起来读起来就像一个正常的英语句子……这也是 RSpec 的理念之一,让测试听起来像对行为的英文描述。

RSpec - 过滤

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

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

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 Filters 很容易地做到这一点。将上述代码更改为以下内容 -

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

现在,让我们尝试 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 格式化程序在运行一些测试时的工作方式,测试通常至少调用一次 expect() 方法。

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

RSpec - 期望

当您学习 RSpec 时,您可能会阅读大量关于期望的内容,起初可能会有点令人困惑。当您看到“期望”一词时,您应该记住两个主要细节 -

  • 期望只不过是在 **it 块**中使用 **expect()** 方法的语句。仅此而已。没有比这更复杂的了。当您有如下代码:**expect(1 + 1).to eq(2)** 时,您的示例中有一个期望。您期望表达式 **1 + 1** 的计算结果为 **2**。措辞很重要,因为 RSpec 是一个 BDD 测试框架。通过将此语句称为期望,可以清楚地表明您的 RSpec 代码正在描述其正在测试的代码的“行为”。其理念是,您正在表达代码应如何运行,以类似文档的方式。

  • 期望语法相对较新。在引入 **expect()** 方法之前(2012 年),RSpec 使用了基于 **should()** 方法的不同语法。上述期望在旧语法中写成这样:**(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()。

广告