aygabtu 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +245 -0
- data/Rakefile +10 -0
- data/aygabtu.gemspec +23 -0
- data/lib/aygabtu.rb +5 -0
- data/lib/aygabtu/generator.rb +64 -0
- data/lib/aygabtu/handle.rb +38 -0
- data/lib/aygabtu/point_of_call.rb +28 -0
- data/lib/aygabtu/route_mark.rb +18 -0
- data/lib/aygabtu/route_wrapper.rb +96 -0
- data/lib/aygabtu/rspec.rb +119 -0
- data/lib/aygabtu/scope/action.rb +38 -0
- data/lib/aygabtu/scope/base.rb +85 -0
- data/lib/aygabtu/scope/named.rb +38 -0
- data/lib/aygabtu/scope/namespace_controller.rb +50 -0
- data/lib/aygabtu/scope/remaining.rb +27 -0
- data/lib/aygabtu/scope/requiring.rb +26 -0
- data/lib/aygabtu/scope/static_dynamic.rb +33 -0
- data/lib/aygabtu/scope/visiting_with.rb +19 -0
- data/lib/aygabtu/scope_actor.rb +104 -0
- data/lib/aygabtu/scope_chain.rb +39 -0
- data/lib/aygabtu/version.rb +3 -0
- data/spec/actions_spec.rb +152 -0
- data/spec/example_spec.rb +61 -0
- data/spec/lib/route_wrapper_spec.rb +140 -0
- data/spec/matching_routes_spec.rb +384 -0
- data/spec/nesting_spec.rb +59 -0
- data/spec/rails_application_helper.rb +8 -0
- data/spec/support/aygabtu_sees_routes.rb +17 -0
- data/spec/support/identifies_routes.rb +18 -0
- data/spec/support/invokes_rspec.rb +93 -0
- data/spec/support/matcher_shims.rb +39 -0
- data/spec/support_spec/identifies_routes_spec.rb +49 -0
- data/spec/visiting_routes_spec.rb +57 -0
- metadata +123 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b6b0b899e98b001d66c42529c6070b7b5cd49129
|
4
|
+
data.tar.gz: 28884477631fe89aa365216d2ad84355d0748c04
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3d45b28adaf1d9ce713ca993aae6ef8405d0378f4e1ec9508c26d75a378f87619443c597c6238b0994735b3c04a16113fa02df0d30774f51d5da2b08e1529873
|
7
|
+
data.tar.gz: cc8db6af0efc3e617c64155f1755c87f0c1d25ff6ab95aa0671415e7055f2da4cebb3b4c3b1505f5906d84c9845d1f5137010f134981a178ac94b62c9dc8bbb5
|
data/.gitignore
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
23
|
+
spec/support/log/
|
24
|
+
_generated_spec.rb
|
data/.travis.yml
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.9.3
|
4
|
+
- 2.1.1
|
5
|
+
env:
|
6
|
+
- RAILS_VERSION="~> 4.1.0" RSPEC_VERSION="~> 2.0"
|
7
|
+
- RAILS_VERSION="~> 4.1.0" RSPEC_VERSION="~> 3.0"
|
8
|
+
- RAILS_VERSION="~> 4.2.0.beta2" RSPEC_VERSION="~> 3.0"
|
9
|
+
- RAILS_VERSION="~> 3.0" RSPEC_VERSION="~> 3.0"
|
10
|
+
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 9elements GmbH
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,245 @@
|
|
1
|
+
# Aygabtu - all your GETs are belong to us!
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/9elements/aygabtu.svg?branch=master "Build Status")](http://travis-ci.org/9elements/aygabtu)
|
4
|
+
|
5
|
+
Aygabtu lets you write simplistic feature tests quickly.
|
6
|
+
|
7
|
+
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.
|
8
|
+
These tests are very easy to set up, but they can only assert simple things like "does the page actually get rendered?".
|
9
|
+
|
10
|
+
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.
|
11
|
+
|
12
|
+
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.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
group :test do
|
19
|
+
gem 'aygabtu', require: false
|
20
|
+
end
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install aygabtu
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
Create `spec/features/aygabtu_features_spec.rb` with the following content:
|
33
|
+
|
34
|
+
```
|
35
|
+
require 'spec_helper' # or whatever is necessary to initialize your Rails app and configure rspec and capybara
|
36
|
+
|
37
|
+
require 'aygabtu/rspec'
|
38
|
+
|
39
|
+
describe "Aygabtu generated features", type: :feature do
|
40
|
+
include Aygabtu::RSpec.example_group_module
|
41
|
+
|
42
|
+
def aygabtu_assertions
|
43
|
+
aygabtu_assert_status_success
|
44
|
+
aygabtu_assert_not_redirected_away
|
45
|
+
end
|
46
|
+
|
47
|
+
# particular example configurations go here
|
48
|
+
|
49
|
+
# must be at the very bottom
|
50
|
+
remaining.static_routes.visit
|
51
|
+
remaining.pend "pending because route needs segments passed"
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
This will get you up and running with
|
56
|
+
|
57
|
+
* 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)
|
58
|
+
* a pending example for every other route
|
59
|
+
|
60
|
+
Roughly, the generated examples will look like this (they are not visible directly):
|
61
|
+
|
62
|
+
```
|
63
|
+
it "generated description here" do
|
64
|
+
visit generated_path
|
65
|
+
aygabtu_assertions
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
Continue reading "Scope, scope chains and actions" for the fundamental notions.
|
70
|
+
|
71
|
+
## Features
|
72
|
+
|
73
|
+
### Scope, scope chains and actions
|
74
|
+
|
75
|
+
This is crucial to understand. Be sure not to miss this section.
|
76
|
+
|
77
|
+
**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:
|
78
|
+
|
79
|
+
```
|
80
|
+
controller(:posts).pend "TBD. Testing posts needs XY done before this can be tackled."
|
81
|
+
```
|
82
|
+
|
83
|
+
creates pending examples for every route routing into `PostsController`. Here, `controller(:posts)` is the scope, and `pend` is the action.
|
84
|
+
|
85
|
+
Scopes can be **chained**. If this reminds you of ActiveRecord query chains, you are exactly on the right track here. For example,
|
86
|
+
|
87
|
+
```
|
88
|
+
namespace(:web).controller(:posts)
|
89
|
+
```
|
90
|
+
|
91
|
+
is a scope matching all routes routing into `Web::PostsController`.
|
92
|
+
|
93
|
+
Aygabtu keeps a **current scope**. You can call any action inside an example group, this will call that action on the current scope.
|
94
|
+
|
95
|
+
Scopes can be **nested**. Call the last method of a scope chain with a block like this:
|
96
|
+
|
97
|
+
```
|
98
|
+
namespace(:web).controller(:posts) do
|
99
|
+
before do
|
100
|
+
...
|
101
|
+
end
|
102
|
+
|
103
|
+
visit_with(some_param: "value")
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
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,
|
108
|
+
|
109
|
+
* the result of the scope chain (`namespace(:web).controller(:posts)` in the above example) is the new current scope
|
110
|
+
* thus, calling an action is affected by that scope
|
111
|
+
* calling a scope method *chains* onto the current scope
|
112
|
+
|
113
|
+
To explain the last point,
|
114
|
+
|
115
|
+
```
|
116
|
+
namespace(:web) do
|
117
|
+
controller(:posts).pend "TBD. Testing posts needs XY done before this can be tackled."
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
would create pending examples for all routes routing into `Web::PostsController`.
|
122
|
+
|
123
|
+
### Treats nonsensical conditions as errors
|
124
|
+
|
125
|
+
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.
|
126
|
+
|
127
|
+
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.
|
128
|
+
|
129
|
+
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.
|
130
|
+
|
131
|
+
Aygabtu keeps track of actions applied to routes, and treats it as an error when
|
132
|
+
|
133
|
+
* 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)
|
134
|
+
* a route is hit twice with the same action (which forces you to structure your examples and keep things tidy)
|
135
|
+
|
136
|
+
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.
|
137
|
+
|
138
|
+
## List of actions
|
139
|
+
|
140
|
+
### `visit` and `visit_with`
|
141
|
+
|
142
|
+
Creates examples for every matching route. Use `visit_with` for passing data for dynamic URL segments and query string parameters.
|
143
|
+
|
144
|
+
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.
|
145
|
+
|
146
|
+
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:
|
147
|
+
|
148
|
+
```
|
149
|
+
controller(:posts) do
|
150
|
+
def post_id
|
151
|
+
post = Post.create
|
152
|
+
post.id
|
153
|
+
end
|
154
|
+
|
155
|
+
visit_with(id: :post_id)
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
### `pend`
|
160
|
+
|
161
|
+
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.
|
162
|
+
|
163
|
+
Pending examples are disabled in such a way that before hooks are not invoked. May actually use RSpec's skip mechanism instead of pending.
|
164
|
+
Unfortunately, the reason does not show up in the output.
|
165
|
+
|
166
|
+
### `ignore`
|
167
|
+
|
168
|
+
No example whatsoever will be generated for matching routes. Requires you to indicate a reason just like `pend`.
|
169
|
+
|
170
|
+
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).
|
171
|
+
|
172
|
+
## List of scope methods
|
173
|
+
|
174
|
+
### `controller` and `namespace`
|
175
|
+
|
176
|
+
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.
|
177
|
+
So if you have a `Customer::ReceiptsController`, translate the name to `customer/receipts` and start thinking about this like a path
|
178
|
+
on a filesystem (where directories are delimited by slashes). Imagine sitting at the root of such a filesytem and looking around.
|
179
|
+
|
180
|
+
When `namespace` is chained or nested, this has the effect of joining path segments. In addition, `namespace` already accepts fragments of paths
|
181
|
+
containing slashes. So for example, by itself,
|
182
|
+
`namespace(:foo).namespace(:bar)` and `namespace('foo/bar')` have the same effect of matching routes to controllers below `Foo::Bar`.
|
183
|
+
|
184
|
+
When the `controller` scope method is used, matching is narrowed down to exactly one controller. So `controller(:root)` matches routes to your
|
185
|
+
`::RootController`, and both `namespace(:foo).controller(:bar)` and `controller('foo/bar')` match routes to `::Foo::BarController`. When
|
186
|
+
`controller` is used, there is no ambiguity as to what controller by the given name your scope narrows down to.
|
187
|
+
|
188
|
+
### `action`
|
189
|
+
|
190
|
+
`action(:show)` matches routes to any `show` action.
|
191
|
+
|
192
|
+
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.
|
193
|
+
|
194
|
+
### `named`
|
195
|
+
|
196
|
+
`named(:posts)` matches the route named `posts` (which you would link to using `posts_path` or `posts_url`).
|
197
|
+
|
198
|
+
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.
|
199
|
+
|
200
|
+
### `visiting_with`
|
201
|
+
|
202
|
+
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.
|
203
|
+
|
204
|
+
### `remaining` and `requiring`
|
205
|
+
|
206
|
+
`remaining` matches routes not used with any action yet, at the point of the call. `requiring` matches routes which need the given route segments.
|
207
|
+
|
208
|
+
You can use them to build constructs like this:
|
209
|
+
|
210
|
+
```
|
211
|
+
controller(:posts) do
|
212
|
+
# let's assume this has a simple resource(:posts) route declaration
|
213
|
+
|
214
|
+
def posts_id
|
215
|
+
...
|
216
|
+
end
|
217
|
+
|
218
|
+
requiring(:id).visiting_with(id: :posts_id).visit # creates examples for all member routes
|
219
|
+
remaining.visit # creates examples for all collection routes (:index and :new)
|
220
|
+
end
|
221
|
+
```
|
222
|
+
|
223
|
+
You can also use `remaining` at the very bottom to pend all remaining routes, see the initial example.
|
224
|
+
|
225
|
+
### `static_routes` and `dynamic_routes`
|
226
|
+
|
227
|
+
* `dynamic_routes` matches routes which have a dynamic segment.
|
228
|
+
* `static_routes` matches routes which have no dynamic segment.
|
229
|
+
|
230
|
+
## Caveats
|
231
|
+
|
232
|
+
* 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.
|
233
|
+
|
234
|
+
## Missing features
|
235
|
+
|
236
|
+
* tests, preferrably against different versions of Rails, RSpec and capybara
|
237
|
+
* support for example metadata (you can have it with a conventional `context` any time)
|
238
|
+
|
239
|
+
## Contributing
|
240
|
+
|
241
|
+
1. Fork it ( https://github.com/[my-github-username]/aygabtu/fork )
|
242
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
243
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
244
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
245
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/aygabtu.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'aygabtu/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "aygabtu"
|
8
|
+
spec.version = Aygabtu::VERSION
|
9
|
+
spec.authors = ["Thomas Stratmann"]
|
10
|
+
spec.email = ["thomas.stratmann@9elements.com"]
|
11
|
+
spec.summary = %q{Feature test generator for GET requests}
|
12
|
+
spec.description = %q{Feature test generator for GET requests, using Capybara and RSpec}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "rspec-rails"
|
22
|
+
spec.add_dependency "capybara"
|
23
|
+
end
|
data/lib/aygabtu.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative 'point_of_call'
|
2
|
+
|
3
|
+
module Aygabtu
|
4
|
+
class Generator
|
5
|
+
def initialize(scope, example_group)
|
6
|
+
@scope, @example_group = scope, example_group
|
7
|
+
end
|
8
|
+
|
9
|
+
generator_methods = [
|
10
|
+
:pending_example,
|
11
|
+
:no_match_failing_example,
|
12
|
+
:example
|
13
|
+
]
|
14
|
+
generator_methods.each do |method|
|
15
|
+
define_method("generate_#{method}") do |*args|
|
16
|
+
code = send(method, *args)
|
17
|
+
@example_group.instance_eval(code, *PointOfCall.file_and_line_at_point_of_call)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def example(route, visiting_data)
|
24
|
+
# it is an error to pass too few data, catch where?
|
25
|
+
statements = [
|
26
|
+
"self.aygabtu_path_to_visit = aygabtu_pass_to_route(#{route.object_id}, #{visiting_data.inspect})",
|
27
|
+
"aygabtu_example_for(aygabtu_path_to_visit)"
|
28
|
+
]
|
29
|
+
message = it_message(route, visiting_data)
|
30
|
+
|
31
|
+
"it(#{message.inspect}) { #{statements.join('; ')} }"
|
32
|
+
end
|
33
|
+
|
34
|
+
def pending_example(route, reason)
|
35
|
+
# We must disable the example in such a way that before hooks are not executed.
|
36
|
+
# I could not find a way of doing this in such a way that RSpec actually takes the reason for
|
37
|
+
# the pending string instead of "Not yet implemented".
|
38
|
+
|
39
|
+
message = pending_message(route)
|
40
|
+
|
41
|
+
"it(#{message.inspect}, skip: #{reason.inspect})"
|
42
|
+
end
|
43
|
+
|
44
|
+
def no_match_failing_example(action)
|
45
|
+
error_message = "No matching route (action was: #{action.inspect}, diagnostics: #{@scope.inspect}"
|
46
|
+
|
47
|
+
"it('is treated as an error by aygabtu when no route matches') { raise #{error_message.inspect} }"
|
48
|
+
end
|
49
|
+
|
50
|
+
def it_message(route, visiting_data)
|
51
|
+
params_message = if visiting_data.empty?
|
52
|
+
"without parameters"
|
53
|
+
else
|
54
|
+
"with parameters #{visiting_data.inspect}"
|
55
|
+
end
|
56
|
+
|
57
|
+
"passes aygabtu assertions for #{route.inspect} #{params_message}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def pending_message(route)
|
61
|
+
"passes aygabtu assertions for #{route.inspect}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|