Did some cleaning & catching up of specs for
railsbestpractices.com yesterday, added quite a number of spec macros to dry up stuff, as well as cutting down the complexity within the spec files. Personally, i really really & really like macros, because it is fun to write & easy to write, it has many beneficial side effects:
#1. Since it is so simple to use macros, everyone in the team is more willing to participate in spec writing. Personal experience has proven that if specs/tests are hard to write, some developers tend to skip it.
#2. Since it is so easy to read, maintenance becomes much easier
#3. Developers are smart people (and should be paid well, but that's another story), by replacing repeated copy & paste & minor edit-here-and-there with macros-writing-and-usage, they feel smarter, a huge morale boast ~> happier team, & when people are happy, they tend to show more love towards the project, take ownership ~> project in a better state.
Btw, macro writing is no rocket-science, let's get started:
#1. Macro for 3rd party declarative
1 2 3 4
| class User < ActiveRecord::Base acts_as_authentic # (blah blah) end
|
Ok, we all know that
authlogic is well tested, so there is really no point in writing specs for it. Yet, how do we know the model is pulling in authlogic support ?? Here's what i've done:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| module RailsBestPractices module Macros
def self.included(base) base.extend(ClassMethods) end
module ClassMethods
def should_act_as_authentic # Get most basic modules included by ActiveRecord::Base basic_included_modules = Class.new(ActiveRecord::Base).included_modules
# Grab the model class model = description.split('::').inject(Object){|klass,const| klass.const_get(const) }
# Get the extra modules included by the model model_included_modules = model.included_modules extra_modules = (model_included_modules - basic_included_modules).map(&:to_s)
# As long as we have any extra module matching the regexp, we can conclude that # :acts_as_authentic has been declared for the model extra_modules.any?{|mod| mod =~ /Authlogic::ActsAsAuthentic::/ }.should be_true end
end
end end
|
The usage in the spec file is:
1 2 3 4 5
| describe User do include RailsBestPractices::Macros should_act_as_authentic # (blah blah) end
|
Of course, i can probably remove the
include statement altogether & do the auto-including elsewhere, but taking this route, other developers may throw WTF-is-should_act_as_authentic error ~> BAD !!
#2. Macro for project-specific declarative
1 2 3 4
| class Post < ActiveRecord::Base include Markdownable # (blah blah) end
|
I wanna make sure the support from the home-baked
Markdownable is pulled in. Since it is home-baked, just making sure the module is mixed in is not good enough, i need to ensure the functionality is there as well. Thus (nested within the above mentioned
RailsBestPractices::Macros::ClassMethods module):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| def should_be_markdownable
# Determine which factory to call factory_id = description.split('::').inject(Object){|klass, const| klass.const_get(const) }. to_s.tableize.singularize.to_sym
# Generate an example group describe "being markdownable" do
it "should generate simple markdown html" do raw = "subject\n=======\ntitle\n-----" formatted = "<h1>subject</h1>\n\n<h2>title</h2>\n" Factory(factory_id, :body => raw).formatted_html.should == formatted end
it "should generate markdown html with <pre><code>" do raw = "subject\n=======\ntitle\n-----\n def test\n puts 'test'\n end" formatted = "<h1>subject</h1>\n\n<h2>title</h2>\n\n<pre><code>def test\n puts 'test'\nend\n</code></pre>\n" Factory(factory_id, :body => raw).formatted_html.should == formatted end
end end
|
And the usage:
1 2 3 4 5
| describe Post do include RailsBestPractices::Macros should_be_markdownable # (blah blah) end
|
#3. Macro for any other vanilla functionality
1 2 3 4 5
| class Implementation < ActiveRecord::Base def belongs_to?(user) user && user_id == user.id end end
|
The macro definition:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| def should_have_user_ownership factory_id = description.split('::').inject(Object){|klass,const| klass.const_get(const) }. to_s.tableize.singularize.to_sym
describe 'having user ownership' do
it 'should belong to someone if he is the owner of it' do someone = Factory(:user) Factory(factory_id, :user => someone).belongs_to?(someone).should be_true end
it 'should not belong to someone if he is not the owner of it' do someone = Factory(:user) Factory(factory_id).belongs_to?(someone).should be_false end
end end
|
Finally, the usage:
1 2 3 4
| describe Implementation do include RailsBestPractices::Macros should_have_user_ownership end
|
Of course, it doesn't make sense to define macro for every functionality available. The general thumb of rule is that when similar code appears more than once, u can consider defining a macro for it. However, if similar-code is to appear more than once, u will probably start by extracting the code & placing it inside a module (eg.
Markdownable), so this probably falls under
#2. Macro for project-specific declarative.
Last but not least, the above macro definition can probably benefit from some refactoring, but i guess i'll leave this exercise to u :]