[rspec-users] testing behaviour or testing code?

Pat Maddox pergesu at gmail.com
Sun Sep 2 02:45:28 EDT 2007

On 8/24/07, David Chelimsky <dchelimsky at gmail.com> wrote:
> On 8/24/07, Pat Maddox <pergesu at gmail.com> wrote:
> > On 8/24/07, David Chelimsky <dchelimsky at gmail.com> wrote:
> > > describe Widget, "class" do
> > >   it "should provide a list of widgets sorted alphabetically" do
> > >     Widget.should_receive(:find).with(:order => "name ASC")
> > >     Widget.find_alphabetically
> > >   end
> > > end
> > >
> > > You're correct that the refactoring requires you to change the
> > > object-level examples, and that is something that would be nice to
> > > avoid. But also keep in mind that in java and C# people refactor
> > > things like that all the time without batting an eye, because the
> > > tools make it a one-step activity. Refactoring is changing the design
> > > of your *system* without changing its behaviour. That doesn't really
> > > fly all the way down to the object level 100% of the time.
> > >
> > > WDYT?
> >
> > I think that example is fine up until the model spec.  The
> > find_alphabetically example should hit the db, imo.  With the current
> > spec there's no way to know whether find_alphabetically actually works
> > or not.  You're relying on knowledge of ActiveRecord here, trusting
> > that the arguments to find are correct.
> Au contrare! This all starts with an Integration Test. I didn't post
> the code but I did mention it.
> > What I've found when I write specs is that I discover new layers of
> > services until eventually I get to a layer that actually does
> > something.  When I get there, it's important to have specs that
> > describe what it does, not how it does it.  In the case of
> > find_alphabetically we care that it returns the items in alphabetical
> > order.  Not that it makes a certain call to the db.
> I play this both ways and haven't come to a preference, but I'm
> leaning towards blocking database access from the rspec examples and
> only allowing it my end to end tests (using Rails Integration Tests or
> - soon - RSpec's new Story Runner).

Now that I've had a chance to play with Story Runner, I want to
revisit this topic a bit.

Let's say in your example you wanted to refactor find_alphabetically
to use enumerable's sort_by to do the sorting.

def self.find_alphabetically
  find(:all).sort_by {|w| w.name }

Your model spec will fail, but your integration test will still pass.

I've been thinking about this situation a lot over the last few
months.  It's been entirely theoretical because I haven't had a suite
of integration tests ;)  Most XP advocates lean heavily on unit tests
when doing refactoring.  Mocking tends to get in the way of
refactoring though.  In the example above, we rely on the integration
test to give us confidence while refactoring.  In fact I would ignore
the unit test (model-level spec) altogether, and rewrite it when the
refactoring is complete.

Here's how I reconcile this with traditional XP unit testing.  First
of all our integration tests are relatively light weight.  In a web
app, a user story consists of making a request and verifying the
response.  Authentication included, you'll be making at most 3-5 HTTP
requests per test.  This means that our integration tests still run in
just a few seconds.  Integration tests in a Rails app are a completely
different beast from the integration tests in the Chrysler payroll app
that Beck, Jeffries, et al worked on.

The second point of reconciliation is that mock objects and
refactoring are two distinct tools you use to design your code.  When
I'm writing greenfield code I'll use mocks to drive the design.  When
I refactor though, I'm following known steps to improve the design of
my existing code.  The vast majority of the time I will perform a
known refactoring, which means I know the steps and the resulting
design.  In this situation I'll ignore my model specs because they'll
blow up, giving me no information other than I changed the design of
my code.  I can use the integration tests to ensure that I haven't
broken any behavior.  At this point I would edit the model specs to
use the correct mock calls.

As I mentioned, this has been something that's been on my mind for a
while.  I find mock objects to be very useful, but they seem to clash
with most of the existing TDD and XP literature.  To summarize, here
are the points where I think they clash:

* Classical TDD relies on unit tests for confidence in refactoring.
BDD relies on integration tests
* XP acceptance tests are customer tests, whereas RSpec User Stories
are programmer tests.  They can serve a dual-purpose because you can
easily show them to a customer, but they're programmer tests in the
sense that the programmer writes and is responsible for those
particular tests.

In the end it boils down to getting stuff done.  After a bit of
experimentation I'm thinking that the process of
1. Write a user story
2. Write detailed specs using mocks to drive design
3. Refactor, using stories to ensure that expected behavior is
maintained, ignoring detailed specs
4. Retrofit specs with correct mock expectations

is a solid approach.  I'd like others to weigh in with their thoughts.


More information about the rspec-users mailing list