混合搭配 RSpec 的各個部分
Myron Marston
2012 年 7 月 23 日RSpec 在上一個主要版本 (2.0) 中被拆分為三個子專案
- rspec-core:測試執行器和主要的 DSL(
describe
、it
、before
、after
、let
、shared_examples
等)。 - rspec-expectations:提供可讀的語法,使用匹配器指定測試的預期結果。
- rspec-mocks:RSpec 的測試替身框架。
這樣做很棒的一點是,它允許你將 RSpec 的各個部分與其他測試函式庫混合搭配。不幸的是,儘管 RSpec 2 已經發布超過 18 個月,但關於如何執行此操作的良好資訊並不多。
我想通過展示一些可能的範例來糾正這個問題。
以下所有範例的頂部都有一些設定/組態程式碼,你可能希望在實際專案中將其提取到 test_helper.rb
或 spec_helper.rb
中。
我建立了一個包含所有這些範例的 github 專案,所以如果你想要一些可以玩的程式碼,請查看一下。
使用 rspec-core 和另一個斷言函式庫
如果你喜歡 RSpec 的測試執行器,但不喜歡 rspec-expectations 提供的語法和失敗輸出,你可以使用 MiniTest 提供的標準函式庫中的斷言。
# rspec_and_minitest_assertions.rb
require 'set'
RSpec.configure do |rspec|
rspec.expect_with :stdlib
end
describe Set do
specify "a passing example" do
set = Set.new
set << 3 << 4
assert_include set, 3
end
specify "a failing example" do
set = Set.new
set << 3 << 4
assert_include set, 5
end
end
輸出
$ rspec rspec_and_minitest_assertions.rb
.F
Failures:
1) Set a failing example
Failure/Error: assert_include set, 5
MiniTest::Assertion:
Expected #<Set: {3, 4}> to include 5.
# ./rspec_and_minitest_assertions.rb:17:in `block (2 levels) in <top (required)>'
Finished in 0.00093 seconds
2 examples, 1 failure
Failed examples:
rspec ./rspec_and_minitest_assertions.rb:14 # Set a failing example
Wrong 是一個有趣的替代方案,它使用單個方法 (帶有區塊的 assert
) 來提供詳細的失敗輸出。
# rspec_and_wrong.rb
require 'set'
require 'wrong'
RSpec.configure do |rspec|
rspec.expect_with Wrong
end
describe Set do
specify "a passing example" do
set = Set.new
set << 3 << 4
assert { set.include?(3) }
end
specify "a failing example" do
set = Set.new
set << 3 << 4
assert { set.include?(5) }
end
end
輸出
$ rspec rspec_and_wrong.rb
.F
Failures:
1) Set a failing example
Failure/Error: assert { set.include?(5) }
Wrong::Assert::AssertionFailedError:
Expected set.include?(5), but
set is #<Set: {3, 4}>
# ./rspec_and_wrong.rb:18:in `block (2 levels) in <top (required)>'
Finished in 0.04012 seconds
2 examples, 1 failure
Failed examples:
rspec ./rspec_and_wrong.rb:15 # Set a failing example
正如這些範例所展示的那樣,你只需設定 expect_with
即可使用替代函式庫。你可以指定 :stdlib
、:rspec
(明確使用 rspec-expectations)或任何模組;該模組將被混入到範例上下文。
使用 minitest 和 rspec-expectations
如果你喜歡使用 MiniTest 執行測試,但更喜歡 rspec-expectations 的語法和失敗輸出,你可以將它們結合起來
# minitest_and_rspec_expectations.rb
require 'minitest/autorun'
require 'rspec/expectations'
require 'set'
RSpec::Matchers.configuration.syntax = :expect
module MiniTest
remove_const :Assertion # so we can re-assign it w/o a ruby warning
# So expectation failures are considered failures, not errors.
Assertion = RSpec::Expectations::ExpectationNotMetError
class Unit::TestCase
include RSpec::Matchers
# So each use of `expect` is counted as an assertion...
def expect(*a, &b)
assert(true)
super
end
end
end
class TestSet < MiniTest::Unit::TestCase
def test_passing_expectation
set = Set.new
set << 3 << 4
expect(set).to include(3)
end
def test_failing_expectation
set = Set.new
set << 3 << 4
expect(set).to include(5)
end
end
輸出
$ ruby minitest_and_rspec_expectations.rb
Run options: --seed 12759
# Running tests:
.F
Finished tests in 0.001991s, 1004.5203 tests/s, 1004.5203 assertions/s.
1) Failure:
test_failing_expectation(TestSet)
[/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-expectations-2.11.1/lib/rspec/expectations/handler.rb:17]:
expected #<Set: {3, 4}> to include 5
2 tests, 2 assertions, 1 failures, 0 errors, 0 skips
讓我們逐步分析整合程式碼
RSpec::Matchers.configuration.syntax = :expect
將所需的 rspec-expectations 語法設定為僅 新的 expect 語法。預設情況下,舊的should
和新的expect
語法都可用。- MiniTest 以不同於其他錯誤的方式處理
MiniTest::Assertion
例外,將它們計為測試失敗,而不是輸出中的錯誤。我們重新分配常數到RSpec::Expectations::ExpectationNotMetError
,以便我們的期望失敗被計為失敗,而不是錯誤。 - MiniTest 的輸出包括斷言總數和每秒斷言數。我們每次調用
expect
時都會新增對assert(true)
的調用,以便計數正確。 - 最後,我們將
RSpec::Matchers
包含到測試上下文中,以使expect
和匹配器可用。
對於其他測試執行器,你可能只需將 RSpec::Matchers
混入到你的測試上下文中即可解決問題 - 其餘的大部分都是 MiniTest 特有的。
使用 minitest 和 rspec-mocks
MiniTest 具有一個模擬物件框架,在 README 中,它是「一個精美小巧的模擬(和樁)物件框架」。它確實精美小巧。但是,rspec-mocks 具有更多功能,如果你喜歡這些功能,你可以輕鬆地將它與 MiniTest 一起使用
# minitest_and_rspec_mocks.rb
require 'minitest/autorun'
require 'rspec/mocks'
MiniTest::Unit::TestCase.add_setup_hook do |test_case|
RSpec::Mocks.setup(test_case)
end
MiniTest::Unit::TestCase.add_teardown_hook do |test_case|
begin
RSpec::Mocks.verify
ensure
RSpec::Mocks.teardown
end
end
class TestSet < MiniTest::Unit::TestCase
def test_passing_mock
foo = mock
foo.should_receive(:bar)
foo.bar
end
def test_failing_mock
foo = mock
foo.should_receive(:bar)
end
def test_stub_real_object
Object.stub(foo: "bar")
assert_equal "bar", Object.foo
end
end
輸出
$ ruby minitest_and_rspec_mocks.rb
Run options: --seed 27480
# Running tests:
..E
Finished tests in 0.002546s, 1178.3189 tests/s, 392.7730 assertions/s.
1) Error:
test_failing_mock(TestSet):
RSpec::Mocks::MockExpectationError: (Mock).bar(any args)
expected: 1 time
received: 0 times
minitest_and_rspec_mocks.rb:25:in `test_failing_mock'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/error_generator.rb:87:in `__raise'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/error_generator.rb:46:in `raise_expectation_error'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/message_expectation.rb:259:in `generate_error'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/message_expectation.rb:215:in `verify_messages_received'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/method_double.rb:117:in `block in verify'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/method_double.rb:117:in `each'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/method_double.rb:117:in `verify'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/proxy.rb:96:in `block in verify'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/proxy.rb:96:in `each'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/proxy.rb:96:in `verify'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/methods.rb:116:in `rspec_verify'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/space.rb:11:in `block in verify_all'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/space.rb:10:in `each'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks/space.rb:10:in `verify_all'
/Users/myron/.rvm/gems/ruby-1.9.3-p194/gems/rspec-mocks-2.11.1/lib/rspec/mocks.rb:19:in `verify'
minitest_and_rspec_mocks.rb:10:in `block in <main>'
3 tests, 1 assertions, 0 failures, 1 errors, 0 skips
此處的整合有點手動,但還不錯
- 在每個測試或範例之前調用
RSpec::Mocks.setup(test)
,以確保設定正確。 - 在每個測試或範例之後調用
RSpec::Mocks.verify
,以便檢查模擬期望。 - 在最後調用
RSpec::Mocks.teardown
,以確保清除對真實物件的任何修改(例如,對於樁)。注意:必須在每次測試(甚至是失敗的測試)之後調用此方法,以防止樁「洩漏」到給定測試之外。這就是我將其放在上面的ensure
子句中的原因。
使用 rspec-core 和另一個模擬函式庫
RSpec 可以輕鬆地與替代的模擬函式庫一起使用。事實上,許多 RSpec 使用者更喜歡 Mocha 而不是 rspec-mocks,並且兩者可以很好地整合在一起
# rspec_and_mocha.rb
RSpec.configure do |rspec|
rspec.mock_with :mocha
end
describe "RSpec and Mocha" do
specify "a passing mock" do
foo = mock
foo.expects(:bar)
foo.bar
end
specify "a failing mock" do
foo = mock
foo.expects(:bar)
end
specify "stubbing a real object" do
foo = Object.new
foo.stubs(bar: 3)
expect(foo.bar).to eq(3)
end
end
輸出
rspec rspec_and_mocha.rb
.F.
Failures:
1) RSpec and Mocha a failing mock
Failure/Error: foo.expects(:bar)
Mocha::ExpectationError:
not all expectations were satisfied
unsatisfied expectations:
- expected exactly once, not yet invoked: #<Mock:0x7fcc01844840>.bar(any_parameters)
# ./rspec_and_mocha.rb:14:in `block (2 levels) in <top (required)>'
Finished in 0.00388 seconds
3 examples, 1 failure
Failed examples:
rspec ./rspec_and_mocha.rb:12 # RSpec and Mocha a failing mock
你可以類似地設定 mock_with :flexmock
或 mock_with :rr
以使用其中一個模擬函式庫。我在此處沒有包含這些範例,因為它們的設定方式完全相同,但是 github 儲存庫 包含此部落格文章中的所有範例,其中也包含它們的範例。
結論
這需要一些額外的準備工作,但大多數 Ruby 的測試工具可以相互整合,不會有任何問題,因此如果你有喜歡的部分和不喜歡的部分,請不要局限於使用所有 MiniTest 或所有 RSpec。使用最符合你需求的測試堆疊。