I’m beginning my crusade to make Rails more testable. I’ll start off with a very small, but very useful plugin that makes testing ActiveRecord classes that use Observers painless.
If you just want the plugin
MIT license, copyright me, yadda yadda
My design lesson for the day
Consider the following Account class:
This bank lets account holders make overdraft withdrawals. They don’t just want to give money away though – at the very least they want to send notices to the account holder. We can stick that logic in the #debit method:
There are a couple problems with that. First of all, it violates the Single Responsibility Principle. The #debit method is not only responsible for changing the account balance, but also for sending overdraft notices. This is just a bad idea in general, and it’s immediately apparent when we try to test it:
The spec fails because #debit calls owner.email, and owner is nil. We can solve this by stubbing the owner:
That’s quite ugly…we’re setting up infrastructure, and we don’t even know why! The example suggests that it’s a basic debit method. So why on earth do we need to stub an owner object?
We can make things a little better by moving the delivery notice out of #debit and into a callback:
That’s better. At least the spec passes without any stubbing.
What happens when we want to add some more behavior? Let’s try generating an account ID based on the owner’s name and SSN:
This spec fails because the #check_account_overdraft callback checks to see if the balance is less than 0.
Clearly there is something wrong. We can’t even test simple account creation because it’s too coupled to the overdraft notification. If you think carefully, you’ll see that overdraft notification shouldn’t be an Account’s responsibility. It may be something the business needs, but it isn’t fundamental to the way an Account behaves. We need to move that behavior elsewhere.
One way is to introduce a Service Layer. A Service Layer allows you to sensibly split up business logic. In our debit example, there are two pieces of business logic. First there’s actually debiting the account, which is domain logic and should be handled by the Account class. Then we have overdraft notification, which is another important business process, but is fundamentally separate from the Account.
We can write an AccountService which wraps it all up:
That works well, and is easy enough to test. It certainly helps us out a great deal by decoupling the Account class from overdraft notification.
There’s something about it I just don’t like though. My main concern is that it introduces another layer, which isn’t necessary for such simple behavior. Ruby is nice and clean so it’s probably not that big of a deal, but at the very least I’ll have to think about whether some behavior should go in the Account or in the AccountService. If it goes in AccountService, can I just edit AccountService directly, or should I decorate it with yet another service? I’d like to get rid of that conceptual overhead.
Observers are great when you want loosely coupled behavior driven by state changes. We can move the overdraft notification to an AccountObserver:
That works out nicely. Now the Account is responsible only for domain logic, and we have another object responsible for notifications.
There’s one problem though. The account ID generation spec blows up again. This is because Rails hooks the observers up as soon as it runs, so now every time an Account gets saved, the AccountObserver kicks off.
The whole point of using an Observer is to decouple the Account’s domain logic from related, but still separate, business logic. But Rails makes you think about Observers when you write tests for the Account.
Enter the No Peeping Toms plugin. It turns off Observers in your test environment, allowing you to write specs without worrying about the Observers – as it should be. Not only that, it also lets you turn on certain Observers for a block of code, letting you write specs for the Observers themselves.
Get it from http://github.com/patmaddox/no-peeping-toms/tree/master
Here’s a spec that shows the account ID generation, basic debit behavior, and hooking up an Observer:
You know that folder you’ve got filled with coding projects that never made it off the ground?
- code snippets that could become libraries, with a few tests
- libraries that could become gems, with some new documentation
- gems that could become side projects, with a bit of refactoring
- side projects that could become products, with the right game plan
You start each project with the best intentions, even big plans for some of them… but then life gets in the way, and you focus your attention on more important things. Then when you get some free time you dream up another project, and the cycle repeats itself.
You know that you can ship – you get all kinds of other projects out the door at work. Why do your open source ideas collect dust and regret?
The rules you apply to get stuff done at work don’t apply to your hobby projects. If you take the objective-driven approach you use in your work and try to use it on your hobby projects, you will continue to fall short of your goals – and keep piling on the guilt.
You can break the cycle, by launching one of your open source project ideas. You can push the spiral upward, by getting one project out the door and moving on to the next one.
You just need to learn a few skills and techniques that will help you dig in to your project graveyard and bring your projects to life.
Get Hack Your Open Source Project for FREE and you will: