librelist archives

« back to archive

Unit Tests Hitting the DB, Obese Models, and Service Objects (oh my!)

Unit Tests Hitting the DB, Obese Models, and Service Objects (oh my!)

From:
James Golick
Date:
2009-12-25 @ 18:48
Hey all. I'm a developer from Vancouver who does a fair bit of rails, among
other things. I try my best to keep pushing the envelope in terms of
practices, so I hope this list will be a great place to discuss ideas with
other thoughtful rails hackers. </intro>

When I read Jay Fields' article about not touching the db in unit tests, I,
like so many others, thought he had gone a bit overboard. As I've learned
more about better testing practices, though, I have started to see the value
in this approach. On all of my new ruby projects, my unit tests are fully
isolated with mocks and stubs fulfilling dependencies.

However, in the rails world, this is not a very widely used practice. Tests
are nearly always inter-dependent and usually hit the database (i.e. not
unit tests).

This is at least partly a result of the "best practice" of stuffing all of
the app's business logic in to its persistence objects. The "fat model"
pattern tends to introduce a high degree of coupling between model objects,
which makes proper stubbing and mocking practices ugly.

I know Pat mentioned service objects in his aloha on rails talk. Although I
don't have any experience implementing this pattern in rails, it strikes me
as the right approach.

Service objects can be decoupled from model objects, and using mocks and
stubs in unit tests becomes straightforward. It also segregates
responsibilities in a more appropriate way.

Persistence objects are for persisting, and service objects are responsible
for implementing the business logic.

What do you think? Have you used this pattern in a sizeable app? Do your
unit tests hit the database?

Re: [rails.excellence] Unit Tests Hitting the DB, Obese Models, and Service Objects (oh my!)

From:
Pat Maddox
Date:
2010-01-04 @ 06:14
My AR::Base subclass unit tests hit the database.  I haven't found it 
worthwhile to isolate those for the most part.  Speed is the main concern.
Most of the time I can write tests in such a way that I don't actually hit
the db, but without introducing an explicit don't-hit-the-db layer.  I 
write a method that performs the business logic but doesn't save.  In my 
controller / service / whatever I call the method and then do an explicit 
save.  This is a big change in style from what I was doing a year ago, 
when I would use callbacks for that sort of thing.  Benefits I see:

* business logic is explicit.  You don't have complicated stuff going on 
magically.  The code does precisely what it says
* tests run faster. A lot of the logic you write doesn't need to be 
persisted, at least not in a unit test. So just make a normal Ruby method 
that does what you want and make sure that things are good
* refactoring is easier because you've decoupled the business logic from 
persistence

One thing that I struggled with for a while was the inherent coupling of 
business & persistence logic in ActiveRecord...or rather I fell into this 
mindset that they were inherently coupled.  They are to some extent, but 
with careful thought you can write an AR::Base subclass in such a way that
the coupling is minimal, and that converting to say DataMapper or 
something else entirely wouldn't be particularly painful.  It's been this 
shift in mindset that's allowed me to keep taking advantage of AR's 
simplicity and power but without driving myself crazy in the long run.

With respect to service and controller unit tests, my preference is to use
mocks there.  Service objects in particular are amenable to mocking due to
the fact that they just coordinate logic among other business objects.

I've not tried moving all/most of the logic out of AR and into service 
objects.  I don't think that's something I'd ever be inclined to do, 
really.

One thing I think could drastically simplify larger models is explicit 
validators.  Often you'll find that some validation needs to be performed 
in a certain context, or in conjunction with another validation, and that 
leads to ugly or confusing code with flags being passed in, funky 
switches, etc.  I wrote a plugin a while back that let me do contextual 
validations but it required some serious monkey-patching and I doubt it 
works anymore.  More recently I'll just have my business methods attach 
error messages right onto the record's errors object.

Pat


On Dec 25, 2009, at 10:48 AM, James Golick wrote:

> Hey all. I'm a developer from Vancouver who does a fair bit of rails, 
among other things. I try my best to keep pushing the envelope in terms of
practices, so I hope this list will be a great place to discuss ideas with
other thoughtful rails hackers. </intro>
> 
> When I read Jay Fields' article about not touching the db in unit tests,
I, like so many others, thought he had gone a bit overboard. As I've 
learned more about better testing practices, though, I have started to see
the value in this approach. On all of my new ruby projects, my unit tests 
are fully isolated with mocks and stubs fulfilling dependencies.
> 
> However, in the rails world, this is not a very widely used practice. 
Tests are nearly always inter-dependent and usually hit the database (i.e.
not unit tests). 
> 
> This is at least partly a result of the "best practice" of stuffing all 
of the app's business logic in to its persistence objects. The "fat model"
pattern tends to introduce a high degree of coupling between model 
objects, which makes proper stubbing and mocking practices ugly.
> 
> I know Pat mentioned service objects in his aloha on rails talk. 
Although I don't have any experience implementing this pattern in rails, 
it strikes me as the right approach.
> 
> Service objects can be decoupled from model objects, and using mocks and
stubs in unit tests becomes straightforward. It also segregates 
responsibilities in a more appropriate way. 
> 
> Persistence objects are for persisting, and service objects are 
responsible for implementing the business logic.
> 
> What do you think? Have you used this pattern in a sizeable app? Do your
unit tests hit the database?

Re: [rails.excellence] Unit Tests Hitting the DB, Obese Models, and Service Objects (oh my!)

From:
Seth Ladd
Date:
2010-01-04 @ 13:16
> required some serious monkey-patching and I doubt it works anymore.  More
> recently I'll just have my business methods attach error messages right onto
> the record's errors object.

Thinking along those lines, why isn't an error thrown immediately if I
try to call a setter method with an invalid valid?  Why do the
validators only fire when trying to save into the database?

Sure, this implies a lot, but the more general question is, if it's a
business rule to ensure a password is at least 4 characters, then why
am I allowed to set a password of 2 characters all the way up to the
point of saving to a DB?

Seth