混合和匹配 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。使用最符合您需求的测试堆栈。