1

我正在使用 Rspec 为我的程序进行一些测试。在规范中,我将类实例化一次,然后使用describecontexts对其进行测试。我遇到了一些有趣的事情,如果似乎在上下文结束时被评估。例如,给定以下类及其相关规范:

class Tmp
  def initialize
    @values = {}
  end

  def modify(new_value1, new_value2)
    @values = {:a => new_value1, :b => new_value2}
  end

  def get_values
    return @values
  end
end

describe Tmp do
  tmp = Tmp.new()

  describe "testing something" do
    context "change value" do

      # First evaluation
      tmp.modify("hi", "bye")
      it {tmp.get_values.should == {:a => "hi", :b => "bye"}}

      # Second evaluation
      tmp.modify("bye", "hi")
      it {tmp.get_values.should == {:a => "bye", :b => "hi"}}
    end
  end
end

使用提供的类和规范,结果如下:

F.

Failures:

  1) Tmp testing something change value 
     Failure/Error: it {tmp.get_values.should == {:a => "hi", :b => "bye"}}
       expected: {:a=>"hi", :b=>"bye"}
            got: {:a=>"bye", :b=>"hi"} (using ==)
       Diff:
       @@ -1,3 +1,3 @@
       -:a => "hi",
       -:b => "bye"
       +:a => "bye",
       +:b => "hi"
     # ./spec/tmp_spec.rb:11:in `block (4 levels) in <top (required)>'

Finished in 0.00211 seconds
2 examples, 1 failure

这很有趣,因为 Rspec 似乎使用tmp中的值评估第一个,因为它位于上下文的末尾。即使在上下文中tmp正在更改其值并且应该通过,Rspec 仍会根据变量在结尾处具有的最后一个值来评估其(我什至在上下文中使用本地原始变量尝试过此操作并且有类似的经验)。

有没有办法解决这个问题并按顺序对其进行评估?或者至少要通过以下测试?我知道我可以使用不同的变量并且它会起作用,但必须有办法解决这个问题。我还想知道这是否是 Rspec 的预期效果。

关于菲利普的回答的更新

通过在单个中进行更改,它会阻止规范通过:

describe Tmp do
  describe "do something" do
    let(:instance) {Tmp.new}

    it 'should be modifiable' do
      instance.modify('hi', 'bye')
      instance.values.should == {a: 'hi', b: 'bye'}
      instance.modify('bye', 'hi')
      instance.values.should == {a: 'bye', b: 'hi'}
    end
  end
end

但是,如果我使用该主题,它似乎会恢复并在第一次失败时失败

describe Tmp do
  describe "do something" do
    let(:instance) {Tmp.new}
    subject{instance.values}

    it 'should be modifiable' do
      instance.modify('hi', 'bye')
      should == {a: 'hi', b: 'bye'}
      instance.modify('bye', 'hi')
      should == {a: 'bye', b: 'hi'}
    end
  end
end

不知道为什么会这样。至少我看到更改应该在it块内,以更好地反映我们正在测试的更改。

4

4 回答 4

5

您不应该在itspecifybeforeletsubject块之外创建实例和操作它们。否则,受试者和其他变量在测试后不会重置。

下面我用几种不同的风格重写了你的规范。有关解释,请参阅内联注释。

class Tmp
  # Exposes the @values ivar through #values
  attr_reader :values

  def initialize
    @values = {}
  end

  def modify(new_value1, new_value2)
    @values = {a: new_value1, b: new_value2}
  end
end

describe Tmp do
  #`let` makes the return value of the block available as a variable named `instance` (technically it is a method named instance, but let's call it a variable).
  let(:instance) { described_class.new }

  # Inside an it block you can access the variables defined through let.
  it 'should be modifiable' do
    instance.modify('hi', 'bye')
    instance.values.should == {a: 'hi', b: 'bye'}
  end

  # or

  # `specify` is like `it` but it takes no argument:
  specify do
    instance.modify('hi', 'bye')
    instance.values.should == {a: 'hi', b: 'bye'}
  end

  # or

  # This is another common way of defining specs, one describe per method.
  describe '#modify' do
    let(:instance) { described_class.new }
    # Here we define the subject which is used implicitly when calling `#should` directly.
    subject { instance.values }
    before { instance.modify('hi', 'bye') }
    it { should == {a: 'hi', b: 'bye' } # Equivalent to calling `subject.should == ...`
  end

  # or

  # When we don't specify a subject, it will be an instance of the top level described object (Tmp).
  describe '#modify' do
    before { subject.modify('hi', 'bye') }
    its(:values) { should == {a: 'hi', b: 'bye' }
  end
end
于 2012-09-21T23:39:19.550 回答
2

rspec执行命令时,RSpec 使用两遍过程:

  1. 首先,它会加载所有规范文件并评估每个示例组。请注意,此时不对it块进行评估;它们被存储以供以后评估。
  2. 一旦加载了所有规范文件,并应用了所有元数据过滤等,RSpec 就会运行每个示例。

起初这可能看起来令人困惑,但实际上,它与您编写的几乎所有 ruby​​ 代码非常相似:您的类是在程序中的某个时间点定义的(通常是在早期,当需要您的代码文件时),以及这些类中的方法稍后被调用。

这是一个纯 ruby​​ 代码片段,它演示了您的示例发生了什么:

# First you define your examples...
class MyExampleGroup
  tmp = {}

  define_method :example_1 do
    puts "example_1 failed (#{tmp.inspect})" unless tmp == {}
  end

  tmp[:a] = 1
  define_method :example_2 do
    puts "example_2 failed (#{tmp.inspect})" unless tmp == { :a => 1 }
  end

  tmp[:b] = 2
end

# Once all examples have been defined, RSpec runs them...
group = MyExampleGroup.new
group.example_1
group.example_2

输出:

example_1 failed ({:a=>1, :b=>2})
example_2 failed ({:a=>1, :b=>2})

每个示例都可以以任何顺序独立运行,这对您的测试套件的稳健性很重要。为此,每个示例最好在自己的“沙箱”中使用自己的被测对象实例执行其操作。 let专为帮助解决此问题而设计;有关更多详细信息,请参阅我的回答。let

于 2012-09-23T06:45:41.630 回答
1

来自 rspec-core 文档:

在后台,示例组是一个类,其中评估传递给描述或上下文的块。传递给它的块在该类的实例的上下文中进行评估。

https://www.relishapp.com/rspec/rspec-core/docs/example-groups/basic-structure-describe-it

这向我表明,ExampleGroup(描述块)中的代码在组被实例化时执行,除了示例本身(it 块)。然后 it 块在 describe 块的上下文中执行。这就是为什么它只看到 的最后一个值tmp

于 2012-09-21T23:42:30.713 回答
1

由于您的实现与&modify的行为之间的相互作用,您基于主题的示例失败了letsubject

这两个方法缓存了它们调用的结果——你显然不希望每次instance被引用时都创建你的类的新实例。这意味着您应该使用 subject 的值,因为它是第一次访问它(由您或由 rspec 显式访问)。

您的主题是instance.values,但调用您的 modify 方法将导致instance.values成为一个新对象(您正在为 @values 分配一个新的哈希,而不是在适当的位置对其进行变异)。您的断言使用的是第一个使用的主题值,因此它们根本没有比较当前值,instance.values因此您的规范失败。

就我个人而言,我认为主题instance.values有点奇怪:你正在与之互动的东西instance是我选择的主题。

于 2012-09-22T08:23:03.303 回答