Are you tired of having DRY code, but tests that seem to babble on "for the length of a bible"? Me too. How about test code that is hard to follow, when Ruby itself is clear as day? I hate it too.
Enter Saki stage left.
Saki lets you do acceptance testing on top of RSpec. It is considerably more terse than Cucumber, but does not sacrifice readability. Saki also does not use Given/When/Then syntax because the thought is that there is little return other than familiarity for Cucumber users.
Well, here's a sample that sets up contexts that create a user and then visit an edit path for that user.
with_existing :user do
on_visiting edit_user_path do
it { should let_me_edit(@user) }
end
end
This code basically injects some "before blocks", so it would look like this in vanilla RSpec:
context "a user exists" do
before { @user = Factory :user }
context "I visit the page for editing that user" do
before { visit "/users/#{@user.id}/user" }
it { should let_me_edit(@user) }
end
end
I believe the Saki example has the following benefits over vanilla RSpec:
- It saves two lines of code.
- It standardizes the code. Whereas a context string might accidentally get out of sync with the code (unmaintained comments, anyone?), with Saki this would probably cause a test to fail.
- It is much more expressive. You read the test and you immediately know what it does.
- Any exceptions to the rule (complicated setups, etc.) would now stick out like sore thumbs, as they should.
The only assumption is that you are using factories instead of fixtures. You also get more out of it in a conventional RESTful application, where the paths don't have too many gotchas.
with_existing
takes a factory name as a symbol and assigns its created object to an instance variable with the same name. It also takes options so you can have blocks start with:
with_existing :user, :state => "happy" do...
on_visiting
preferably uses some dynamic functions for establishing a path: new_X_path
, Xs_path
, edit_X_path
, X_path
and new_X_path
. In these cases, substitute X for the resource name (e.g. new_user_path
).
Note that for examples like edit_user_path
, it behaves with a slight difference from the rails route helpers, because it assumes that there already exists an instance variable named @user
. Since the edit_user_path
call occurs when there is no @user
, we can't mention it explicitly.
For cases where the resource is nested, these path helpers have a :parent => parent_resource option. This lets you set up blocks like:
on_visiting auctions_path(:parent => :user) do ...
on_visiting
also takes a path as a string, or a lambda that executes within a before block to set up the path. It also takes a symbol which is the name of a method name. This is useful when the code is dependent on an instance variable for path creation.
path_for_user = lambda { user_path(@user) }
on_visiting path_for_user do ...
or you can do
def my_user_path
user_path(@user)
end
on_visiting :my_user_path do ...
on_following_link_to
works the same as on_visiting, but it first validates that the link exists, and then follows it.
where
is a function taking as a parameter a lambda to execute in the before block.
def self.creating_a_user
lambda {
@user = Factory.build @user
fill_in "user[email]", :with => @user.email
click_button "Create"
}
end
on_following_link_to new_user_path do
where creating_a_user do
specify { page.should have_content(@user.email) }
end
end
Obviously the return for this is where you have functions acting as "reusable steps" in the style of Cucumber. In addition your "before blocks" are more expressive.
Finally, to simplify setting up integration tests, anything you wrap in an integrate
block (like describe
) sets the test type to acceptance. This is the default describe
function of the generators, but feel free to use the regular describe block as long as you set its :type option to :acceptance.
Saki installs with two steps. First, add to your Gemfile:
gem 'saki'
Then to fill out the directories run:
rails generate saki:install
You can generate new acceptance tests with rails generate saki:spec SPEC_NAME
. This automatically generates tests like
require File.dirname(__FILE__) + '/acceptance_helper'
integrate "author resource" do
on_visiting new_author_path do
specify { fail "not implemented" }
end
with_existing :author do
on_visiting edit_author_path do
specify { fail "not implemented" }
end
on_visiting author_path do
specify { fail "not implemented" }
end
on_visiting authors_path do
specify { fail "not implemented" }
end
end
end
They'll get there :). Saki is extracted from some spec helpers I used in moving from Cucumber to Steak. Once I realized they also work as helpers for vanilla RSpec acceptance testing I made them a separate gem.
I haven't pimped that up yet, but will at some point. Personally I'm a "green-dot" guy and just care what line a test fails on.
The motivation behind my migration from Cucumber and to Saki, are described in blog posts Encumbered by Cucumber, Introducing Saki.
The generators are stolen directly from Steak with some adjustments.
- Fork the project.
- Make your feature addition or bug fix.
- Add tests for it. This is important so I don't break it in a future version unintentionally.
- Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
- Send me a pull request. Bonus points for topic branches.
MIT License