Aygabtu - all your GETs are belong to us!
Aygabtu lets you write simplistic feature tests quickly.
It provides a DSL on top of rspec and capybara that can be used to enumerate a rails application's routes and auto-generate feature tests. These tests are very easy to set up, but they can only assert simple things like "does the page actually get rendered?".
Features that are valuable and profitable enough should still be written conventionally, aygabtu is not a silver bullet for feature tests. Since aygabtu embraces rspec, it should be simple to migrate a feature from using aygabtu to a full-blown rspec/capybara feature.
Aygabtu uses code generation under the hood, but tries to be guard-friendly: Guard should be able to re-run failed examples by line number in many situations.
Installation
Add this line to your application's Gemfile:
group :test do
gem 'aygabtu', require: false
end
And then execute:
$ bundle
Or install it yourself as:
$ gem install aygabtu
Usage
Create spec/features/aygabtu_features_spec.rb
with the following content:
require 'spec_helper' # or whatever is necessary to initialize your Rails app and configure rspec and capybara
require 'aygabtu/rspec'
describe "Aygabtu generated features", type: :feature do
include Aygabtu::RSpec.example_group_module
def aygabtu_assertions
aygabtu_assert_status_success
aygabtu_assert_not_redirected_away
end
# particular example configurations go here
# must be at the very bottom
remaining.static_routes.visit
remaining.pend "pending because route needs segments passed"
end
This will get you up and running with
- an example for every route that does not require any dynamic segment to be passed, which visits that route and asserts the HTTP status is 200 and the url did not change (because of a redirect)
- a pending example for every other route
Roughly, the generated examples will look like this (they are not visible directly):
it "generated description here" do
visit generated_path
aygabtu_assertions
end
Continue reading "Scope, scope chains and actions" for the fundamental notions.
Features
Scope, scope chains and actions
This is crucial to understand. Be sure not to miss this section.
Scopes define rules and filters to be applied to routes. When an action is called for a scope, it affects all routes filtered by the scope and uses rules defined by it. Basic example:
controller(:posts).pend "TBD. Testing posts needs XY done before this can be tackled."
creates pending examples for every route routing into PostsController
. Here, controller(:posts)
is the scope, and pend
is the action.
Scopes can be chained. If this reminds you of ActiveRecord query chains, you are exactly on the right track here. For example,
namespace(:web).controller(:posts)
is a scope matching all routes routing into Web::PostsController
.
Aygabtu keeps a current scope. You can call any action inside an example group, this will call that action on the current scope.
Scopes can be nested. Call the last method of a scope chain with a block like this:
namespace(:web).controller(:posts) do
before do
...
end
visit_with(some_param: "value")
end
This creates a new example group (exactly what happens when you call context
in rspec). You can use this to set up test preconditions in a before
block, as indicated in the example above. Inside this context,
- the result of the scope chain (
namespace(:web).controller(:posts)
in the above example) is the new current scope - thus, calling an action is affected by that scope
- calling a scope method chains onto the current scope
To explain the last point,
namespace(:web) do
controller(:posts).pend "TBD. Testing posts needs XY done before this can be tackled."
end
would create pending examples for all routes routing into Web::PostsController
.
Treats nonsensical conditions as errors
When you apply an action to a scope which does not match any route, most probably you made a mistake, and aygabtu treats it that way. This avoids your aygabtu examples diverging from your application.
Some scopes can break up into multiple scopes, and each component must match a route by itself. See the documentation for the individual scope methods.
In many situations you can see aygabtu raising an exception when you try to do things that do not make sense. Aygabtu prefers yelling at you over you yelling at your computer because some weird conditions create unexplicable behaviour that is hard to debug.
Aygabtu keeps track of actions applied to routes, and treats it as an error when
- a route is hit with two different actions (having both a pending example and a regular one for the same route means you probably missed something)
- a route is hit twice with the same action (which forces you to structure your examples and keep things tidy)
As an exception, you can create multiple regular examples for the same route using visit
and visit_with
. Please consider if using a vanilla rspec/capybara spec would make more sense in that case.
List of actions
visit
and visit_with
Creates examples for every matching route. Use visit_with
for passing data for dynamic URL segments and query string parameters.
Data can be passed as an argument to visit_with
and using the visiting_with
scope method. The deeper the nesting or chaining (the call to visit
or visit_with
is always the deepest), the higher the precedence.
Data is passed as a hash, where keys are parameter or dynamic segment names, and values are passed after being converted to strings. Symbol values are special: they are interpreted as method names within the example and used to obtain the actual value. Example:
controller(:posts) do
def post_id
post = Post.create
post.id
end
visit_with(id: :post_id)
end
pend
Creates a pending example for every matching route. Requires you to indicate a reason as the only parameter. This is a good thing since it means the reason for the decision to pend the example(s) is kept in the source.
Pending examples are disabled in such a way that before hooks are not invoked. May actually use RSpec's skip mechanism instead of pending. Unfortunately, the reason does not show up in the output.
ignore
No example whatsoever will be generated for matching routes. Requires you to indicate a reason just like pend
.
As a short-hand, you can use covered!
instead of ignore
for routes that need no aygabtu example because somebody has already written a regular feature test that covers them (but please be honest to yourself and don't use covered!
just because it allows you to omit the reason).
List of scope methods
controller
and namespace
These two go hand in hand. They determine how routes routing to a controller are matched, depending on the fully qualified name of the controller.
So if you have a Customer::ReceiptsController
, translate the name to customer/receipts
and start thinking about this like a path
on a filesystem (where directories are delimited by slashes). Imagine sitting at the root of such a filesytem and looking around.
When namespace
is chained or nested, this has the effect of joining path segments. In addition, namespace
already accepts fragments of paths
containing slashes. So for example, by itself,
namespace(:foo).namespace(:bar)
and namespace('foo/bar')
have the same effect of matching routes to controllers below Foo::Bar
.
When the controller
scope method is used, matching is narrowed down to exactly one controller. So controller(:root)
matches routes to your
::RootController
, and both namespace(:foo).controller(:bar)
and controller('foo/bar')
match routes to ::Foo::BarController
. When
controller
is used, there is no ambiguity as to what controller by the given name your scope narrows down to.
action
action(:show)
matches routes to any show
action.
When called with multiple arguments, the resulting scope breaks up internally, and for each name, a route must match. So action(:show, :index)
is just a short-hand for using action
twice.
named
named(:posts)
matches the route named posts
(which you would link to using posts_path
or posts_url
).
When called with multiple arguments, the resulting scope breaks up internally, and for each name, a route must match. So named(:posts, :comments)
is just a short-hand for using named
twice.
visiting_with
When the visit
or visit_with
actions are used, the scope uses the given parameters for building the URLs. See the documentation for the visit
action.
remaining
and requiring
remaining
matches routes not used with any action yet, at the point of the call. requiring
matches routes which need the given route segments.
You can use them to build constructs like this:
controller(:posts) do
# let's assume this has a simple resource(:posts) route declaration
def posts_id
...
end
requiring(:id).visiting_with(id: :posts_id).visit # creates examples for all member routes
remaining.visit # creates examples for all collection routes (:index and :new)
end
You can also use remaining
at the very bottom to pend all remaining routes, see the initial example.
static_routes
and dynamic_routes
dynamic_routes
matches routes which have a dynamic segment.static_routes
matches routes which have no dynamic segment.
Caveats
- With the standard assertions configured, Aygabtu will happily accept a rails error page as long as the HTTP status is 200. Somebody should find out how these can be reliably told apart from regular result pages, so the default assertions can be improved. Until then, you should try to add an assertion that checks for a common element on pages, like a footer element.
Missing features
- tests, preferrably against different versions of Rails, RSpec and capybara
- support for example metadata (you can have it with a conventional
context
any time)
Contributing
- Fork it ( https://github.com/[my-github-username]/aygabtu/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request