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...
In cases where you would like to indicate ownership, you may use that_has_a
or that_has_an
to indicate that your object belongs to an object from your outer with_existing
. For example:
with_existing :user do
that_has_an :article do
that_has_a :comment do
creates a user with an article in its "articles" collection, and adds to that article's "comments" collection a new comment.
with_signed_in
is similar to with_existing
but after creating the object it passes it into a sign_in
method:
with_signed_in :admin 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 proc 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 = proc { 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 proc to execute in the before block.
def self.creating_a_user
proc {
@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, we assume that rather than using "describe", you wrap everything in a "feature" block. This is a Capybara shortcut (taken from Steak), for setting the enclosed specs to a type of request
.
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 request tests with rails generate saki:spec SPEC_NAME
. This automatically generates tests like
require File.dirname(__FILE__) + '/request_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
If you want to use Saki for generating integration tests for your scaffolding, simply add to your development.rb file
require "generators/saki/integration_generator"
Then, provided that your integration library is set to :rspec, Saki will create integration tests for your scaffolding for complete test coverage. Note that these cases are left simple for now, but can be built up upon feedback. Also you might need to implement additional functions depending on your use case.
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 request 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.
To work with Ruby 1.9.2 the where
function takes procs but not lambdas.
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