aygabtu 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,10 @@
1
+ ## 0.2.0 (2015-04-28)
2
+
3
+ Breaking change:
4
+
5
+ - Semantics of the remaining scope changed
6
+
7
+ ## 0.0.1
8
+
9
+ Initial version, not released on Rubygems.
10
+
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in aygabtu.gemspec
4
+ gemspec
5
+
6
+ gem 'rake'
7
+ gem 'rails', ENV['RAILS_VERSION'] || '~> 0'
8
+ gem 'rspec', ENV['RSPEC_VERSION'] || '~> 0'
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
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ rescue LoadError
7
+ end
8
+
9
+ task default: :spec
10
+
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,5 @@
1
+ require "aygabtu/version"
2
+
3
+ module Aygabtu
4
+ # Your code goes here...
5
+ end
@@ -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