What is the โ€œrightโ€ way to test functions that call methods in new instances?

I seem to have come across literature, citing the fact that this is bad practice using RSpec methods any_instance_of(e.g. expect_any_instance_of). The docs resources even list these methods in the "Working with Obsolete Code" section ( http://www.relishapp.com/rspec/rspec-mocks/v/3-4/docs/working-with-legacy-code/any-instance ), which means that I should not write new code using this.

I feel like I regularly write new specifications that rely on this feature. A great example is any method that creates a new instance and then calls a method on it. (In Rails, where MyModel is an ActiveRecord) I regularly write methods that do something like this:

def my_method
    my_active_record_model = MyModel.create(my_param: my_val)
    my_active_record_model.do_something_productive
end

Usually I write my specifications looking for a call do_something_productiveusing expect_any_instance_of. eg:.

expect_any_instance_of(MyModel).to receive(:do_something_productive)
subject.my_method

The only other way I can see is with a bunch of stubs like this:

my_double = double('my_model')
expect(MyModel).to receive(:create).and_return(my_double)
expect(my_double).to receive(:do_something_productive)
subject.my_method

Nevertheless, I think this is worse because: a) it is longer and slower to write, and b) it is a much more fragile and white box than the first method. To illustrate the second point, if I change my_methodto the following:

def my_method
    my_active_record_model = MyModel.new(my_param: my_val)
    my_active_record_model.save
    my_active_record_model.do_something_productive
end

then the double version of spec breaks, but the version any_instance_ofworks fine.

So my questions are, how do other developers do this? Is my use approach any_instance_ofdisapproving? And if so, why?

+4
source share
3

, , . stubbing/mocking allow, expect create, create , . , , , .

, Feature Envy. MyModel :

class MyModel < ActiveRecord::Base
  def self.create_productively(attrs)
    create(attrs).do_something_productive
  end
end

def my_method
  MyModel.create_productively(attrs)
end

# in the spec
expect(MyModel).to receive(:create_productively)
subject.my_method

create_productively , , .

, RSpec , .

+1

, :

docs " " (http://www.relishapp.com/rspec/rspec-mocks/v/3-4/docs/working-with-legacy-code/any-instance), , , .

. Mocking/stubbing - . , mocking/stubbing " ", db, API .

, ? , , . / . , , , , .. , *_any_instance_of , . , , , , . *_any_instance_of , , , , .

, , , :

my_double = double('my_model')
expect(MyModel).to receive(:create).and_return(my_double)
expect(my_double).to receive(:do_something_productive)
subject.my_method

, . , , my_method , . , , , , - my_method. , .

+1
def self.my_method(attrs)
  create(attrs).tap {|m| m.do_something_productive}
end

# Spec   
let(:attrs) { # valid hash }

describe "when calling my_method with valid attributes" do
  it "does something productive" do
    expect(MyModel.my_method(attrs)).to have_done_something_productive
  end
end

, #do_something_productive.

: , . , .

I tend to reserve mocks / stub for external dependencies (e.g. API calls) or when using interfaces that have been defined but not implemented.

0
source

All Articles