Developing Spree extension with TDD

Here at Nebulab, we have just built an extension (Spree Subscriptions) that adds to Spree features needed to buy and manage subscription based items, ideal for magazines or similar products. We developed this extension for one of our clients and it has been our first official extension. Starting from scratch allowed us to better undestand how to develop Spree extensions using a TDD process and we wanted to share some thoughts and best practices about this topic. To make a rapid overview, in this article we will create a new extension, using some spree-subscriptions real scenarios and code, that adds a checkbox to the products edit page to denote if it is a subscribable product (a magazine). We will start writing tests and then we will make them pass.
Before starting
Choose the right tools
A Spree extension is essentially a simple Rails engine that has the spree_core gem (another engine) as dependency; this means we can use any preferred library to test it. Since Spree uses Rspec, FactoryGirl and Capybara we think that the best approach is to use the same tools in order to make Spree developers feel comfortable with tools they are used to work with and, eventually, reproduce or use some code already present in spree core. For example, in our extension we can reuse spree core factories or create new factories that have them as parents.
Create the new extension
It’s suggested to read Creating Extension Spree Official Guide before starting, which explores better this topic.
$ gem install rails
$ gem install spree
Now we can create the extension using the spree extension
command.
$ cd ~/path/to/your/Code
$ spree extension Subscriptions
$ cd spree_subscriptions
Note: This guide refers to Spree 1.1 version of Spree. I think the only difference with the 1.2 (actually RC) will be with handling spree_auth
that will not be a strict dependency of spree-core, letting us choose our favourite authentication system.
Generate a resource
Althoughe we have not to generate a resource for our extension example scenario, it could be useful to see how to do it and how to automatically create rspec files with a simple command. We have to add rspec gem to our extension Gemfile:
group :test do
gem 'rspec'
end
After a bundle install
we can lauch:
$ rails g resource spree/subscription
invoke active_record
create db/migrate/20120705112710_create_spree_subscriptions.rb
create app/models/spree/subscription.rb
create app/models/spree.rb
invoke rspec
create spec/models/spree/subscription_spec.rb
invoke controller
create app/controllers/spree/subscriptions_controller.rb
invoke erb
create app/views/spree/subscriptions
invoke rspec
create spec/controllers/spree/subscriptions_controller_spec.rb
invoke helper
create app/helpers/spree/subscriptions_helper.rb
invoke rspec
create spec/helpers/spree/subscriptions_helper_spec.rb
invoke assets
invoke js
create app/assets/javascripts/spree/subscriptions.js
invoke css
create app/assets/stylesheets/spree/subscriptions.css
invoke resource_route
route namespace :spree do resources :subscriptions end
And the tree will be correctly generated with rspec files too.
Include local factories
By default Spree extensions require the factories used for spree core testing. In fact into the spec_helper we can find:
require 'spree/core/testing_support/factories'
These factories can be very useful because we are sure we are modeling our test following real spree objects but if we want to test our extension we probably want to add some new factories. To add this feature we can simply add this lines after the core factories require:
Dir["#{File.dirname(__FILE__)}/factories/**/*.rb"].each do |f|
fp = File.expand_path(f)
require fp
end
Now we can add our custom factories into spec/factories
directory.
We can also extend existing spree core factories using them as parents for our new factories, for example:
FactoryGirl.define do
factory :subscribable_product, :parent => :product do
subscribable true
end
end
Using spree preferences in our tests
Testing an extension will probably let us bump into the setup of some spree preferences. Spree Core is tested resetting preferences to default values each time they are used to ensure tests always run in a default environment. To reproduce this behavior in our extension we can simply create (copy) a support method into /spec/support/preferences.rb
def reset_spree_preferences
Spree::Preferences::Store.instance.persistence = false
config = Rails.application.config.spree.preferences
config.reset
yield(config) if block_given?
end
that will allow us to setup required preferences in our tests:
before(:each) do
reset_spree_preferences do |config|
config.default_country_id = create(:country).id
config.site_name = "my dummy test store for subscriptions"
end
end
Add auth gem to test as an admin user
To test as admin interface functionality we have to sign in as admin user. To do it we have to require spree_auth into our lib/spree_subscriptions.rb
file. which now will be like this:
require 'spree_core'
require 'spree_auth'
require 'spree_subscriptions/engine'
In order to complete login easily throught integration tests we can define another support method “sign_in_as!”, taken directly from spree_auth gem:
def sign_in_as!(user)
visit '/login'
fill_in 'Email', :with => user.email
fill_in 'Password', :with => 'secret'
click_button 'Login'
end
Run tests on extension
To test our extension we need to crate a dummy test app that will reside into spec/dummy/
. This app will contain everything needed to execute the tests from our extension. Before running rspec for the first time and every time we make a structural change to the extension (like changing or adding a migration) we need to create the test app with this command:
$ rake test_app
Once completed we can run tests within our extension with:
$ rspec spec
Add some integration tests
Let’s say we want to test the ability of an admin user to flag a product as subscribable through the admin panel. We need to create some integation tests first.
In spec/requests/admin/product_spec.rb
:
require 'spec_helper'
describe "Product" do
context "on edit" do
it "should be selected as subscribable" do
product = create(:product)
user = create(:admin_user, :email => "[email protected]")
sign_in_as!(user)
visit spree.admin_path
click_link "Products"
within('table.index tr:nth-child(2)') { click_link "Edit" }
check('product_subscribable')
click_button "Update"
page.should have_content("successfully updated!")
page.has_checked_field?('product_subscribable').should == true
end
end
end
This test will not pass because the checkbox doesn’t exist yet. We can create with this Deface override:
Deface::Override.new(:virtual_path => "spree/admin/products/_form",
:name => "adds_subscribable_to_product",
:insert_bottom => "[data-hook='admin_product_form_right']",
:partial => "spree/admin/products/subscription_field")
As you can see, this override will render a partial in the right part of the product edit form. This partial contains:
<%= f.field_container :subscribable do %>
<%= f.label :subscribable %><br />
<%= f.check_box :subscribable %>
<% end %>
Test fails agin. This time because there are no setter and getter method for “subscribable” attribute. It’s time to add some model tests.
Add some model tests
Into spec/model/product_spec.rb
we can add this code that test the existence of subscribable method and its default value:
require 'spec_helper'
describe Spree::Product do
let(:subscribable_product) { Factory(:subscribable_product) }
let(:simple_product) { Factory(:simple_product) }
it "should respond to subscribable method" do
subscribable_product.should respond_to :subscribable
end
it "should be subscribable" do
subscribable_product.subscribable.should be_true
end
it "should have subscribable to false by default" do
simple_product.subscribable?.should be false
end
end
In order to let these tests pass, we have to add the subscribable field to the product table with a migration, then we have to recreate the test app to update its database that will include this last migration.
$ rails g migration add_subscribable_to_spree_products subscribable:boolean
$ rake test_app
We can try to run test but they will continue to fail. This time the issue is with mass assignment of subscribable attribute. To solve this problem we have to extend the product model creating a product decorator file in app/models/spree/product_decorator.rb
:
Spree::Product.class_eval do
attr_accessible :subscribable
end
These simple example tests pass so we can now be sure an admin user can set a product as subscribable.
Get a screenshot on test failure
Sometime we found really useful to view a screenshot (in .html format) of the page a test fails. There is a gem that does exactly the job: ‘capybara-screenshot’. To add it to our spree extension we can modify our Gemfile
adding the gem to the test group:
group :test do
gem 'capybara-screenshot', :require => false
end
and requiring it after rspec/rails
in the spec_helper
...
require 'rspec/rails'
require 'capybara-screenshot'
require 'ffaker'
...
After a test failure we can find the html screenshot in /spec/dummy/tmp/capybara/
folder. Keep in mind that these files will be deleted everytime we run rake test_app
.
Configure TravisCI for our extension
If we need a CI for our extension, we can simply configure TravisCI. The only extra configuration required to let it handle correctly an extension is related to the rake test_app
command that we have to execute before running our tests. Here it is a sample .travis.yml file:
before_install: gem install bundler --pre
before_script:
- "bundle exec rake test_app"
script: "DISPLAY=:99.0 bundle exec rspec spec"
notifications:
email:
- [email protected]
branches:
only:
- master
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
Conclusion
We want to get straight to the point: tests saved us a lot of time developing our first extension, expecially when we have to test orders. Imagine testing a critical feature that depends on checkout; every time you change something in the code you should test an order with guest checkout, an order for an already logged in user, an order for a user that signs up during the checkout, an admin order completion and probably other use cases we’ve not considered. Making things work without tests would surely be a pain.