Thank you for all of the extra details.

BTW, The create is not the ActiveRecord create. Also we are not using
Rails, just ruby. We don’t have a view. The closest we come to a controller
object/class is the class that the create_accounts method is in. Say for
the sake of argument, that that class is named BatchController.

That’s fine. We don’t need to pretend it is Rails at all. I was just going
off the very common AR create interface on that guess.

To avoid using any_instance, I rewrote the call inside the loop to
ProspectAccount.create_from_vendor(epoch,
vendor), but that only moves the problem to the create_from_vendor method.

It’s difficult to judge things based only off of this small code snippet.
Without a better understanding of your domain models (nothing to do with
Rails or ActiveRecord) and how they are related I can only guess at some
alternatives. Given your latest sample it seems really that you are writing
some sort of service object.

I agree with Myron. Given it seems that a prospect account is tightly
coupled to a vendor and an epoch; and you’ve stated create is of your own
API design, I would pose the question: _”Is there a need to ever call create
directly on a ProspectAccount instance; outside of create_from_vendor?”

If the answer is “no”. I would strongly suggest going with what Myron
proposed. Make create a private implementation detail and promote the tests
for it up to the create_from_vendor call. Then only use create_from_vendor
in the code.

For example:

class ProspectAccount
  def self.create_from_vendor(epoch, vendor)
    new(vendor).create(epoch)
  end

private

  def create(epoch)
    # ...
  endend
RSpec.describe ProspectAccount do
  describe "creating an account from a vendor with epoch" do
    # move all tests you had for `create` here
  endend

Given all of your well thought out reasoning behind the design decisions so
far, if the answer is “yes” we could approach things from another angle.
Based only on these snippets, it seems you cannot have a ProspectAccount
instance without a Vendor instance. We could also consider an API design
with:

class Vendor
  def self.generate_prospects(start_time)
    new_vendors(start_time).map { |v| ProspectAccount.new(v) }
  endend
RSpec.describe Vendor do
  it "with no matching start time generating prospects returns an
empty array" do
    expect(Vendor.generate_prospects(some_non_matching_time)).to eq []
  end

  it "with a matching start time generating prospects returns the new
prospects" do
    # This may need a custom matcher or some sort of equality checker
for prospects
    expect(Vendor.generate_prospects(matching_time)).to match_array
some_expected_prospects
  endend

That wraps the factory up to the thing which the prospect depends on. This
frees up your service object to be:

class BatchController
  def self.create_accounts(epoch, start_time)
    Vendor.generate_prospects(start_time).each do |prospect|
      prospect.create(epoch)
    end
  end
end
RSpec.describe BatchController do
  context "with some prospects" do
    it "creating accounts transmits each prospect" do
      a_start_time = :start_time
      an_epoch = :epoch
      prospects = [spy(ProspectAccount), spy(ProspectAccount)]
      allow(Vendor).to
receive(:generate_prospects).with(a_start_time).and_return(prospects)

      BatchController.create_accounts(an_epoch, a_start_time)

      prospects.each { |prospect| expect(prospect).to
have_received(:create).with(an_epoch) }
    end
  endend

​

On Thu, May 28, 2015 at 3:07 PM, Marlin Pierce <mpie...@capterra.com> wrote:

> Yes that does help.
>
> I understand the second bullet point.  It might not be a bug if the create
> was called with a different vendor object, but it would be unless the other
> vendor object represented the same vendor in reality, (same name, same
> primary key).
>
> I'm not sure I grok how the communication pattern is complex.
>
> Maybe create is not the right name for the method.  It's more like write,
> or transmit.  Really, its transmit the call to the server for a create
> action.
>
> The design of the code is for a ProspectAccount object to initialize its
> state from a given Vendor object.  Then, knowing its state, send the create
> information to the web server acting as our queuing system.
>
>
> Going to lengths to avoid any_instance and its variations, I had come up
> with:
>
> let(:vendor) { ::Vendor.new(...) }
> let(:prospect) { ProspectAccount.new(vendor) }
>
> it 'calls create instance method' do
>   allow(ProspectAccount).to receive(:new).and_return(prospect)
>   expect(prospect).to receive(:create).and_return(true)
>
>   ProspectAccount.create_from_vendor(epoch, vendor)
> end
>
> I bulk a little at stubbing the ::new method, as it is a fundamental to
> the OOP language.
>
>
> One strange point, (and maybe I'm not grokking what you are saying) is
> that it seems that your example of returning a double, is stubbing the
> object-under-test, and then you stub the :create method, which is stubbing
> the method on the object-under-test.
>
> Further, I can see problems with stubbing the object-under-test, but not
> with writing an expectation to call a method when I am trying to verify
> that the method gets called.
>
>
>
> class BatchController
>   def create_accounts(epoch, start_time)
>     vendors = Vendor.new_vendors(start_time)
>
>     vendors.each do |vendor|
>       ProspectAccount.new(vendor).create(epoch)
>     end
>   end
> ...
> end
>
> describe '#create' do
>
>   let(:vendors) { [ ::Vendor.new(...), ::Vendor.new(...), ::Vendor.new
> (...) ] }
>   let(:prospect) { ProspectAccount.new(vendor) }
>
>   it 'calls create instance method' do
>     allow(Vendor).to receive(:new_vendors).and_return(vendors)
>     expect_any_instance_of(ProspectAccount).to receive(:create).exactly(3
> ).times.and_return(true)
>
>     BatchController.create(epoch, start_time)
>   end
> end
>
>
> Here, it does not seem that I'm stubbing the object-under-test.
>
> Maybe I am, since I am stubbing every instance of ProspectAccount, and I'm
> testing if those objects call the #create method.
>
>  --
> You received this message because you are subscribed to the Google Groups
> "rspec" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to rspec+unsubscr...@googlegroups.com.
> To post to this group, send email to rspec@googlegroups.com.
> To view this discussion on the web visit
> https://groups.google.com/d/msgid/rspec/bcb391cc-989b-499a-83b5-6fb1174220e7%40googlegroups.com
> <https://groups.google.com/d/msgid/rspec/bcb391cc-989b-499a-83b5-6fb1174220e7%40googlegroups.com?utm_medium=email&utm_source=footer>
> .
>
> For more options, visit https://groups.google.com/d/optout.
>

-- 
You received this message because you are subscribed to the Google Groups 
"rspec" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to rspec+unsubscr...@googlegroups.com.
To post to this group, send email to rspec@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/rspec/CAKCESdjfYOmQ_aaLKnfxuQrHKx8%2Bea%3Dh%2BhMDkbx0w5hg84Ff0Q%40mail.gmail.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to