- RSpec 教程
- RSpec - 首頁
- RSpec - 簡介
- RSpec - 基本語法
- RSpec - 編寫規範
- RSpec - 匹配器
- RSpec - 測試替身
- RSpec - 存根
- RSpec - 鉤子
- RSpec - 標籤
- RSpec - 主題
- RSpec - 助手
- RSpec - 元資料
- RSpec - 過濾
- RSpec - 期望
- RSpec 資源
- RSpec 快速指南
- RSpec - 有用資源
- RSpec - 討論
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 關鍵字,用於定義“示例”。示例基本上是一個測試或測試用例。同樣,與 describe 和 context 一樣,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()。