I completely agree that testability is a huge benefit, and if changing code
structure makes that easier then I'm all for it.  My statement was only
meant to imply that I didn't find testability to become any easier by
refactoring this particular method, and absent that I didn't see any reason
to split it up.  But if testability is actually improved, I'd be eager to
make changes appropriately.

I looked over your code in the gist (which, by the way I really appreciate
you taking the time to write out), but I don't see how this tests the method
properly.  The result of the else condition is never actually tested.
Specifically, I thing the tests would all pass, even if
find_or_add_something looked like this:

  def self.find_or_add_something(inputs)
    if x = self.find_something(inputs)
      x
    else
      self.add_something(inputs)
      "blah"
    end
  end

(if it isn't clear, the difference is that I'm not calling find_something in
the else condition and am instead just returning "blah").

This is because the last test "#find_or_add_something when find something
returns nil returns the result of find_something" doesn't actually specify
that find_something returns nil the first time it is called.  So that last
spec describes what I want to test but what it actually tests is the same
thing as "#find_or_add_something when find something returns a Thing returns
the Thing" [line 39].

At least this is what it seems like to me; if I'm missing something
glaringly obvious (possible) please do point it out to me.

Hopefully with this concrete example of a set of specs that you've provided
my question is a bit clearer.



On Tue, Jul 21, 2009 at 5:49 AM, David Chelimsky <dchelim...@gmail.com>wrote:

> Hi Barun,
>
> On Tue, Jul 21, 2009 at 1:05 AM, Barun Singh<baru...@gmail.com> wrote:
> > I certainly appreciate the thoughtful and lengthy reply, but I think it
> > misses the main part of my problem -- perhaps my original explanation of
> my
> > issue was lacking in clarity.  This isn't a question of refactoring; I
> can
> > easily refactor the method as you describe but this doesn't resolve the
> > issue (indeed, it just leads to an increased number of public methods in
> the
> > model with no real benefit).
>
> You want to know your code works, right? Testability _is_ a real
> benefit. Obviously everything needs to be balanced, and if you can do
> things in a pure API, non-invasive way, you're going to end up with a
> more flexible result. But sometimes we have to make tradeoffs.
>
> > So, to more clearly illustrate my question,
> > forget about my original method and instead let's say I have three
> methods
> > as follows:
> >
> > def self.find_something(inputs)
> >   .. do stuff here
> > end
> >
> > def self.add_something(inputs)
> >   .. do stuff here
> > end
> >
> > def self.find_or_add_something(inputs)
> >   x = self.find_something(inputs)
> >   if !x
> >     self.add_something(inputs)
> >     x = self.find_something(inputs)
> >   end
> >   x
> > end
> >
> > I can easily spec the find_something and add_something methods so I know
> > that they work correctly.  My question is, how do I write a spec for
> > find_or_add_something in a way where I don't have to re-test the behavior
> of
> > the individual find_something and add_something methods a second time,
> and
> > can just test the overall logic of that method?
>
> This is _very_ invasive and brittle, and I'd likely not approach it
> this way myself, but it works:
>
> http://gist.github.com/151226
>
> HTH,
> David
>
> >
> > It's helpful to know that Mocha is available to assist with this, but I'd
> > rather not switch to an entirely different framework for mocks & stubs
> just
> > because of this one method...
> >
> >
> >
> > On Mon, Jul 20, 2009 at 11:45 PM, Stephen Eley <sfe...@gmail.com> wrote:
> >>
> >> On Mon, Jul 20, 2009 at 8:33 PM, Barun Singh<baru...@gmail.com> wrote:
> >> >
> >> > I want to find a way to write a spec for this method that does both of
> >> > these
> >> > things:
> >> > (1)  stub out the do_something method (that method has its own specs)
> >> > (2)  not stub out the logic in the else statement, (there is some
> >> > complex
> >> > logic in there involving sql queries on multiple tables and i
> explicitly
> >> > want to make it touch the database so that I can examine the state of
> >> > the
> >> > database after the method is run in my spec.  a lot of complex stubs
> >> > would
> >> > be required to do this while also having the spec that is actually
> >> > useful)
> >>
> >> Okay, first off -- I think part of your problem is that your focus is
> >> too short.  It sounds like you're asking how to write different specs
> >> for various lines of the _internals_ of this method.  That's too
> >> fine-grained.  Spec the method from the _outside:_ write specs that
> >> say "If I give the method these inputs, this should happen."  Then
> >> test that the end result of the method's execution was what you
> >> expect.
> >>
> >> I'm betting that probably sounds really difficult, given the "complex
> >> logic" you're describing.  And that would be my second, more important
> >> tip: if your method is so complicated that you can't spec it from the
> >> outside, you should take that as a sign.  Spaghetti code is usually
> >> very hard to spec.
> >>
> >> Reconsider your problem.  Try breaking it down into smaller, more
> >> easily specified pieces, and then recompose them into the logic that
> >> you need.  I don't know your business rules; it looks to me like the
> >> basic task here is "Keep twiddling with the database until I get what
> >> I'm looking for, as many times as that takes."  I don't know why
> >> that's necessary, but if it is, recursion doesn't seem like the most
> >> elegant answer to me.  Recursive calls are usually made in small
> >> functions that don't have side effects.  This is the opposite.
> >>
> >> What would I do instead?  Again, without knowing your specific
> >> business, I'd probably break it into pieces, and then turn the
> >> requirement inside out and do it with a loop:
> >>
> >> def self.find_something(inputs)
> >>  <find something in db based on inputs>
> >> end
> >>
> >> def self.add_something(inputs)
> >>  <add something to db based on inputs>
> >> end
> >>
> >> And then, wherever you *would* have called mymethod(inputs), you can
> >> instead write this, which will loop zero or more times:
> >>
> >> add_something_to_db(inputs) until x = find_something(inputs)
> >> x.do_something
> >>
> >> ...Whether or not you still want to wrap that in a method probably
> >> depends on whether you need to call it more than once.  But I'll bet
> >> you that the "add_something" and "find_something" methods are both
> >> easier to spec from the outside.  If they're still too complex, then
> >> break them down too.
> >>
> >>
> >> Oh, and final trivia note:
> >>
> >> > any thoughts on how to accomplish this?  two approaches that would
> make
> >> > sense but don't seem to be possible as far as i know are:
> >> > (A)  stub out the do_something method for any instance of the class (i
> >> > can't
> >> > stub it out for a particular record because the records don't exist at
> >> > the
> >> > time of calling the method and I'm not stubbing the creation process)
> >> > (B)  stub out "mymethod" only for the second time it is called
> >> > (obviously i
> >> > can't stub the method out entirely since that's the method i'm
> testing)
> >>
> >> You can do both of these with the Mocha mocking library, if you really
> >> want to.  It puts an "any_instance" method on Class that you can use
> >> to mock or stub; and you can specify ordered chains of results to
> >> return.
> >>
> >> But again, you shouldn't need to if your methods are atomic enough.
> >> If 'do_something' properly belongs outside the scope of what you're
> >> testing, then maybe that's a sign that you shouldn't put it in the
> >> same block of code.  And if you find a need to declare that your
> >> method should behave entirely differently on a second invocation,
> >> maybe it shouldn't be one method.
> >>
> >> Simplify relentlessly.  Once I figured out how to make my code
> >> simpler, I found that I wasn't mocking nearly as much as I used to.
> >>
> >>
> >> --
> >> Have Fun,
> >>   Steve Eley (sfe...@gmail.com)
> >>   ESCAPE POD - The Science Fiction Podcast Magazine
> >>   http://www.escapepod.org
> >> _______________________________________________
> >> rspec-users mailing list
> >> rspec-users@rubyforge.org
> >> http://rubyforge.org/mailman/listinfo/rspec-users
> >
> >
> > _______________________________________________
> > rspec-users mailing list
> > rspec-users@rubyforge.org
> > http://rubyforge.org/mailman/listinfo/rspec-users
> >
> _______________________________________________
> rspec-users mailing list
> rspec-users@rubyforge.org
> http://rubyforge.org/mailman/listinfo/rspec-users
>
_______________________________________________
rspec-users mailing list
rspec-users@rubyforge.org
http://rubyforge.org/mailman/listinfo/rspec-users

Reply via email to