Showing posts with label rails. Show all posts
Showing posts with label rails. Show all posts

Friday, April 23, 2010

Improving macro registration for Remarkable 4

Since one of the emerging design principle for Remarkable 4 is to shadow Rails 3, and Rails 3's biggest strength is its modularity, I've decided to split Remarkable into components following the Rails 3 modules. For now, we have remarkable (core), remarkable-activemodel, and remarkable-activerecord. I plan for remarkable-rails to be split into remarkable-rack and maybe remarkable-actioncontroller.

However, with all of this, we now introduced the idea of macro collections depending on other macro collections. However, the way these macros are loaded into Rspec does not easily support this proliferation of modules.

Currently, macro developer has to do this:

Remarkable.include_matchers!(Remarkable::ActiveModel, Rspec::Core::ExampleGroup)


But seriously, do the developers need to know they are targeting Rspec::Core::ExampleGroup? This should be a default, with it being overridden by people who know what they are doing.

The other thing is that due to the modularity of Rails 3, it makes sense to have a set of macros built on top of other macros. So for example, ActiveRecord macros are built on top of ActiveModel macros. Not everyone will want to include the entire Rails stack just to use the macros, and we certainly don't want to expose a target (at least, not without having to dig).

Here is a mock API of what I'm thinking of:

The developer adds this:

Remarkable.register_macros(:activemodel, Remarkable::ActiveModel)
Remarkable.register_macros(:activemodel, Remarkable::ActiveModel,
:target => Rspec::Core::ExampleGroup)
Remarkable.register_macros(:activerecord, Remarkable::ActiveRecord,
:depends_on => :activemodel

This makes it easy for plugin developers:

Remarkable.register_macros(:foreigner, Remarkable::Foreigner,
:depends_on => :activerecord)
Remarkable.register_macros(:paperclip, Remarkable::Paperclip,
:depends_on => :activerecord)

If you are using only pieces, then you can active the macros:

Remarkable.activate!(:activerecord)
Remarkable.activate!(:paperclip)

Which will idempotently include the macros and its dependencies into Rspec2.

Remarkable::Rails, being the gregarious gem it is, will call:

Remarkable.activate!

... which will load up all of the macros. This would be in keeping with the expected convention for Rails while still providing modularity.

I've opened up this issue on the github to discuss this further.

Thursday, April 22, 2010

Remarkable 4.0.0.alpha1

I finally got a Remarkable 4 alpha1 one out. Here is how to use it in your project:

gem install remarkable remarkable_activemodel remarkable_activerecord --pre


There is no 4.0.0.alpha1 release for remarkable_rails. Please upvote and comment this github issue if this is important to you.

To install:

# In Gemfiles
group :test do
gem 'rspec'
gem 'rspec-rails'
gem 'remarkable', '>=4.0.0.alpha1'
gem 'remarkable_activemodel', '>=4.0.0.alpha1'
gem 'remarkable_activerecord', '>=4.0.0.alpha1'
end


and add this in your spec/spec_helper.rb

Remarkable.include_matchers!(Remarkable::ActiveModel, Rspec::Core::ExampleGroup)
Remarkable.include_matchers!(Remarkable::ActiveRecord, Rspec::Core::ExampleGroup)


Please note, you will need to have rspec 2.0.0.alpha7 to run this, either installed in system gems or in the bundle.

Caveats


While I ported as many of the features of Remarkable 3.3 over, there were some things that were dropped:


  • support_attributes and passing attributes into a describe or context block no longer works. Rspec2 provides a metadata functionality allowing you to pass metadata in examples and example groups, and this conflicts with the semantics for support_attributes. Quite frankly, I never even knew this feature existed until I attempted this port; it isn't something I ever needed since I do not write models in a way where its behavior changes drastically based on state. When it does change, I typically write out the descriptions long-hand.

    If there are enough upvotes for this issue, or someone who cares enough decide to submit a patch, we can add this back in. Any implementation will use the Rspec2 metadata though.


  • pending example groups are baked into Rspec2, so I took it out of Remarkable. The specs for the macros show they work.

  • I took out the validations that were implemented in ActiveModel and moved them over to a brand-new, shiny remarkable_activemodel gem. You will need both remarkable_activemodel and remarkable_activerecord to use the validations. I plan on making the packaging better so you can easily just activate ActiveModel macros whenever you load in ActiveRecord models. This will allow people to use the standard validation macros against any ActiveModel::Validations-compliant models, which I expect the vast majority of the NoSQL and ActiveResource models to implement.



Please post any issues to the Remarkable issue tracker

Tuesday, April 20, 2010

Taking over the Remarkable gem

Remarkable is an awesome gem. If you use Rspec for a Rails project, this macro framework is certainly something to consider.

The classic way of using Rspec to test your ActiveRecord model looks like this:

class Person < ActiveRecord::Base
belongs_to :organization

validates_presence_of :name
validates_presence of :email
validates_uniqueness_of :email
end

# In spec/models/person_spec.rb
describe Person do
before(:each) do
@valid_attributes = {
:name => "Name"
:email => "email@example.com"
}
end

it "should create a new instance given valid attributes" do
Person.create!(@valid_attributes)
end

it "should validate presence of :name" do
@person = Person.new(@valid_attributes.except(:name)
@person.should_not be_valid
@person.errors.on(:name).should_not be_nil
end

it "should validate presence of :email" do
@person = Person.new(@valid_attributes.except(:email)
@person.should_not be_valid
@person.errors.on(:email).should_not be_nil
end

it "should validate uniqueness of :email" do
@original_person = Person.create!(@valid_attributes)
@clone = Person.create!(@valid_attributes)
@person.should_not be_valid
@person.errors.on(:email).should_not be_nil
end
end

[ See Gist ]

What this does is create a template model that has all the required attributes. We have an example that makes sure it is valid. Then for every validation, we create a modified attribute set so we can test each attribute in isolation.

This gets old very fast. Trust me, I've written many tests like this since Rspec got popular in 2008.

So how can we make this easier?

We can try using fixtures. The problem with fixtures is that (1) we have to manage our own primary keys and argue with the database and (2) you would have to load the fixture in, create a clone, and attempt to test each of the validation.

We can try mocking. This would give us the isolation when we stub out the mock model, but that's essentially another way of writing a valid_attributes. Besides, now we're digging deep into the internals of ActiveRecord.

We can use factories. I like factories, in particular, I currently use the Machinist in combination with Faker gem and Forgery. This gives me the advantage of having a blueprints.rb file I can include in db/seeds.rb to generate data I want to drop into my views. However, we are still essentially testing the ActiveRecord framework.

This is where Remarkable comes in. The above Rspec condenses down to:

describe Person do
it { should validate_presence_of :name }
it { should validate_presence_of :email }
it { should validate_uniqueness_of :email }
end

Done.

[ See Gist]

Notice that these macros shadow the ActiveRecord declarations. This is true for all the other macros, including association macros such as should have_many, should belong_to. Instead of focusing on boilerplate test examples, you focus on writing examples for custom code and really weird business logic.

Remarkable has come a long way from its beginning as a "Shoulda for Rspec". Since version 3.0, Remarkable has been refactored into a generic framework for creating Rspec macros. Remarkable::ActiveRecord and Remarkable::Rails were extensions of this macro. As a result, we saw some other macros: a remarkable for paperclip, mongo, and datamapper. There is also an internationalization package that lets you internationalize your rspec.

However, with big refactoring of Rails 3, followed by the big refactoring of Rspec 2, Remarkable needs some work. Carlos Brando, the founding author of the Remarkable gem has been looking for a new maintainer, and has passed the torch to me. As I stated in the Remarkable mailing list and on Rails-talk, my plan is to release a Remarkable 4.0 that is compatible with Rails 3 and Rspec 2, however, it will not be backwards compatible with Rails 2 and Rspec 1. Remarkable 3.3 will be around for that.

If you use Remarkable and plan on using it for your Rails 3 project, please speak up now. We're going to take advantage of the mass refactoring to do some refactoring of our own.