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の値を使用して最初のitを評価するように見えるため、これは興味深いことです。コンテキスト内でtmpがその値を変更しており、パスする必要がありますが、Rspec は変数が最後に持つ最後の値に基づいてその値を評価します (コンテキスト内のローカル プリミティブ変数でこれを試してみたところ同様の経験があります)。

これを回避し、順番評価する方法はありますか? または、少なくとも次のテストに合格するには?さまざまな変数を使用でき、それが機能することはわかっていますが、これを回避する方法が必要です。また、これが Rspec の意図した効果であるかどうかも知りたいです。

フィリップの答えに関する更新

単一のitブロック内で変更を行うことにより、仕様は次のようにパスします。

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

itspecifybeforelet、およびsubjectブロックの外でインスタンスを作成して操作するべきではありません。それ以外の場合、サブジェクトとその他の変数はテスト後にリセットされません。

以下では、いくつかの異なるスタイルを使用して仕様を書き直しました。説明については、インライン コメントを参照してください。

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

rspecRSpec は、コマンドを実行するときに 2 パス プロセスを使用します。

  1. まず、すべてのスペック ファイルをロードし、各サンプル グループを評価します。この時点ではitブロックは評価されないことに注意してください。それらは後で評価するために保存されます。
  2. すべての仕様ファイルが読み込まれ、すべてのメタデータ フィルタリングなどが適用されると、RSpec は各サンプルを実行します。

これは最初は混乱するように思えるかもしれませんが、実際には、あなたが書くほとんどすべての 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

の実装と&modifyの動作の間の相互作用のために、サブジェクトベースの例は失敗しますletsubject

これらの2つのメソッドは、呼び出しの結果をキャッシュします。参照されるたびにクラスの新しいインスタンスが作成されることは明らかに望ましくありませんinstance。これは、(明示的に、またはrspecによって)最初にアクセスされたときのsubjectの値を使用する必要があることを意味します。

サブジェクトはですinstance.valuesが、modifyメソッドを呼び出すinstance.valuesと、新しいオブジェクトになります(@valuesに新しいハッシュを割り当てるのではなく、その場で変更します)。アサーションはsubjectの最初に使用された値を使用しているため、現在の値をまったく比較していないinstance.valuesため、仕様は失敗します。

個人的には、主題を持っているのinstance.valuesは少し奇妙だと思います。あなたが対話しているのはinstance、それが私の主題の選択になるようにするためです。

于 2012-09-22T08:23:03.303 に答える
1

rspec-core ドキュメントから:

内部的には、サンプル グループは、describe または context に渡されたブロックが評価されるクラスです。それに渡されたブロックは、そのクラスのインスタンスのコンテキストで評価されます。

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

これは、ExampleGroup (describe ブロック) 内のコードが、サンプル自体 (it ブロック) を除いて、グループがインスタンス化されるときに実行されることを示唆しています。次に、it ブロックが記述ブロックのコンテキストで実行されます。そのため、 の最後の値のみが表示されますtmp

于 2012-09-21T23:42:30.713 に答える