混合和匹配 RSpec 的部分

Myron Marston

2012 年 7 月 23 日

RSpec 在最后一个主要版本 (2.0) 中被拆分为三个子项目。

其中一个很酷的事情是,它允许您将 RSpec 的部分与其他测试库混合和匹配。不幸的是,尽管 RSpec 2 已经发布了 18 个多月,但关于如何做到这一点的信息并不多。

我想通过展示一些可能性来纠正这一点。

下面所有示例在顶部都有一些设置/配置代码,您可能希望在实际项目中将其提取到 test_helper.rbspec_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 混合到您的测试上下文中即可——大多数其他内容都是特定于 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-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 :flexmockmock_with :rr 来使用其中一个模拟库。我没有在这里包含这些示例,因为它们的配置方式完全相同,但 包含本博文所有示例的 github 仓库 中也包含它们的示例。

结论

这需要一些额外的努力,但大多数 ruby 测试工具可以毫无问题地相互集成,因此如果有一些您喜欢和不喜欢的东西,您不必局限于使用所有 MiniTest 或所有 RSpec。使用最符合您需求的测试堆栈。