neverfails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.swp
2
+ *.pyc
3
+ *.egg-info
4
+ build/
5
+ dist/
6
+
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/HISTORY.md ADDED
@@ -0,0 +1,8 @@
1
+ v0.0.1
2
+ ======
3
+
4
+ New Features
5
+ ------------
6
+
7
+ * First working version
8
+
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,287 @@
1
+ Behavior-driven development (BDD) consists of [five steps](http://cukes.info/):
2
+
3
+ 1. Describe behavior in plain text
4
+ 2. Write a step definition
5
+ 3. Run and watch it fail
6
+ 4. Write code to make the step pass
7
+ 5. Run again and see the step pass
8
+
9
+ Neverfails is a proof of concept to reduce this list to **two steps**:
10
+
11
+ 1. Describe behaviour in plain text
12
+ 2. Run and watch it pass
13
+
14
+ With neverfails, step definitions do not simply check whether the existing code satifies the required behaviour or not. They also **write the code** to make them pass.
15
+
16
+ Neverfails involves an ambitious idea: code generation based on specifications. This idea does not depend on a specific platform or programming language. In principle, it could be implemented with any framework. The current claudiob/neverfails@rails branch uses Rails as a web framework and Ruby as the programming language. The master claudiob/neverfails@master branch, on the other hand, is investigating the same approach using Python and Django.
17
+
18
+ Behavior-driven development in Rails
19
+ ====================================
20
+
21
+ Before approaching neverfails, it is important to understand how Behaviour-Driven Development (BDD) typically takes place within a Rails project.
22
+
23
+ Step 1 (Describe behavior in plain text)
24
+ ----------------------------------------
25
+
26
+ Say we want to create a web store for a *grocery* store, with a distinct page for each product. The *apples* page, for instance, will list the types and quantities of apples currently in store, showing "No apples left" if there are none left in the market. This scenario can be described as:
27
+
28
+ ``` cucumber
29
+ Feature: Apples
30
+ Scenario: No apples left
31
+ Given there are no apples
32
+ When I browse the list of apples
33
+ Then I should see the text "No apples left"
34
+ ```
35
+
36
+ Having described behavior in plain text, we create a blank Rails project and make use of [cucumber](https://github.com/gabrielfalcao/lettuce) and [capybara](https://github.com/jnicklas/capybara) to run the steps.
37
+
38
+ The following commands set up a new `grocery` Rails project with a basic SQLite database, and a bundle installation with cucumber and capybara:
39
+
40
+ ``` bash
41
+ rails new grocery -JT
42
+ cd grocery
43
+ rm public/index.html
44
+ rm public/images/rails.png
45
+ echo -e '\ngem "cucumber"' >> Gemfile
46
+ echo -e '\ngem "cucumber-rails"' >> Gemfile
47
+ bundle install
48
+ rails g cucumber:install
49
+ echo "require 'cucumber/rails'
50
+ Capybara.default_selector = :css
51
+ ActionController::Base.allow_rescue = false" >| features/support/env.rb
52
+ sed -i '' -e's/<<: \*test/<<: *development/' config/database.yml
53
+ rake db:create
54
+ echo -e "Feature: Apples
55
+ Scenario: No apples left
56
+ Given there are no apples
57
+ When I browse the list of apples
58
+ Then I should see the text \"No apples left\"
59
+ " > features/apples.feature
60
+ ```
61
+
62
+ Step 2 (Write step definitions)
63
+ -------------------------------
64
+
65
+ To make Rails aware of what the actions in the scenario actually mean, we can either write new step definitions, or import some library that translates common actions into Python commands. One such popular library for Web applications is [capybara](https://github.com/jnicklas/capybara).
66
+
67
+ For the sake of the `grocery` example, we define the three steps of the `No apples left` scenario as follows:
68
+
69
+ * *Given there are no apples*: this step passes if a model called 'Apple' exist and if there are no instances of this model in the database
70
+ * *When I browse the list of apples*: this step passes if a page exists listing apples and if I can open that page in a browser
71
+ * *Then I should see the text "No apples left"*: this step passes if I see the text "No apples left" in that page
72
+
73
+ The file `fails_steps.rb` in this package contains these definition in Ruby and capybara code.
74
+
75
+ Step 3 (Run and watch it fail)
76
+ ------------------------------
77
+
78
+ The following commands copy the content of this file in the project and run the steps again:
79
+
80
+ ``` bash
81
+ echo -e "# MODELS
82
+
83
+ Given /^there are no (\\S+?)\$/ do |objects|
84
+ model_name = objects.classify
85
+ Given \"there is a model called #{model_name}\"
86
+ Given \"there are no instances of that model\"
87
+ end
88
+
89
+ Given /^(?:|there is )a model called (.+?)\$/ do |model_name|
90
+ assert ActiveRecord::Base.connection.tables.include?(model_name.tableize),
91
+ \"No model found called #{model_name}\"
92
+ @last_model = model_name.constantize
93
+ end
94
+
95
+ Given /^(?:|there are )no instances of that model\$/ do
96
+ @last_model.delete_all
97
+ end
98
+
99
+ # NAVIGATION
100
+
101
+ When /^I browse the list of (.+?)\$/ do |models|
102
+ Given \"there is a page listing #{models}\"
103
+ When \"I navigate to that page\"
104
+ end
105
+
106
+ Given /^there is a page listing (.+?)\$/ do |models|
107
+ Given \"there is a page with URL /#{models}\"
108
+ end
109
+
110
+ Given /^there is a page with URL (.+?)\$/ do |url|
111
+ assert Rails.application.routes.routes.collect(&:conditions).
112
+ collect{|route| route[:path_info] =~ url }.any?,
113
+ \"No URL pattern found matching #{url}\"
114
+ \$last_url = url
115
+ end
116
+
117
+ When /^I navigate to that page\$/ do
118
+ visit \$last_url
119
+ end
120
+
121
+ # CONTENT
122
+
123
+ Then /^I should see the text \"([^\"]*)\"\$/ do |text|
124
+ begin
125
+ page.should have_content(text)
126
+ rescue Test::Unit::AssertionFailedError => e
127
+ raise e.class, \"The text \\\"#{text}\\\" was not found in the current page\"
128
+ end
129
+ end
130
+ " > features/step_definitions/fails_steps.rb
131
+ cucumber RAILS_ENV=development
132
+ ```
133
+
134
+ The result is the following, indicating that the *first* step has failed:
135
+
136
+ ```
137
+ No model found called Apple. (Test::Unit::AssertionFailedError)
138
+ ```
139
+
140
+ Step 4 (Write code to make the three steps pass)
141
+ -------------------------------------------------
142
+
143
+ To make the first step pass, we need to create an Apple model and store it in the database:
144
+
145
+ ``` bash
146
+ rails g model apple
147
+ rake db:migrate
148
+ cucumber RAILS_ENV=development
149
+ ```
150
+
151
+ The result is now the following, indicating that the *second* step has failed:
152
+
153
+ ```
154
+ No URL pattern found matching /apples. (Test::Unit::AssertionFailedError)
155
+ ```
156
+
157
+ To make the second step pass, we need to create a URL pattern matching "apples/" that points to a blank HTML page:
158
+
159
+ ``` bash
160
+ rails g controller Apples index
161
+ sed -i '' '
162
+ /get "apples\/index"/ a\
163
+ match "/apples" => "apples#index"
164
+ ' config/routes.rb
165
+ cucumber RAILS_ENV=development
166
+ ```
167
+
168
+ The result is now the following, indicating that the *third* step has failed:
169
+
170
+ ```
171
+ The text "No apples" left was not found in the current page (Test::Unit::AssertionFailedError)
172
+ ```
173
+
174
+ To make the third step pass, we need to add the text "No apples left" to the page that lists apples:
175
+
176
+ ``` bash
177
+ echo "No apples left" >| app/views/apples/index.html.erb
178
+ cucumber RAILS_ENV=development
179
+ ```
180
+
181
+ Step 5 (Run again and see the step pass)
182
+ ----------------------------------------
183
+
184
+ Finally, the three steps pass and running them again returns the message:
185
+
186
+ ``` cucumber
187
+ Feature: Apples
188
+ Scenario: No apples left
189
+ Given there are no apples
190
+ When I browse the list of apples
191
+ Then I should see the text "No apples left"
192
+
193
+ 1 feature (1 passed)
194
+ 3 steps (3 passed)
195
+ ```
196
+
197
+ Neverfails does all of this, so you don't have to (TO COMPLETE)
198
+ ===============================================================
199
+
200
+ The `grocery` example shows that Behaviour-Driven Development is time-consuming even for very small applications, with an empty model and a view showing one sentence.
201
+ Time is spent watching the tests fail and writing snippets of code that are common to every web application (creating a model, filling view with text and so on).
202
+
203
+ Neverfails reduces this time by automatically creating the missing snippets of code when a step fails.
204
+
205
+
206
+ Step 1 (Describe behavior in plain text)
207
+ ----------------------------------------
208
+
209
+ Continuing with the grocery example, say we want to add this new scenario:
210
+
211
+ ``` cucumber
212
+ Feature: Bananas
213
+ Scenario: No bananas left
214
+ Given there are no bananas
215
+ When I browse the list of bananas
216
+ Then I should see the text "No bananas left"
217
+ ```
218
+
219
+ The following commands add the previous scenario to the grocery project and include neverfails to the project:
220
+
221
+ ``` bash
222
+ echo -e "Feature: Bananas\n\tScenario: No bananas left\n\t\tGiven there are no bananas\n\t\tWhen I browse the list of bananas\n\t\tThen I should see the text \"No bananas left\"" > features/bananas.feature
223
+ echo -e '\ngem "neverfails"' >> Gemfile
224
+ bundle install
225
+ echo -e "\nrequire 'neverfails'" >> features/support/env.rb
226
+ echo -e "\nRails.configuration.cache_classes = false" >> features/support/env.rb
227
+ ```
228
+
229
+ Step 2 (Run and watch it pass)
230
+ ------------------------------
231
+
232
+ Both the `apples` and the `bananas` scenario can be run with the command:
233
+
234
+ ``` bash
235
+ cucumber features/
236
+ ```
237
+
238
+ The `apples` scenario passes since we already wrote all its required. The `bananas` scenario, though, passes as well:
239
+
240
+ ``` cucumber
241
+ Feature: Apples
242
+ Scenario: No apples left
243
+ Given there are no apples
244
+ When I browse the list of apples
245
+ Then I should see the text "No apples left"
246
+
247
+ Feature: Bananas
248
+ Scenario: No bananas left
249
+ Given there are no bananas
250
+ Creating tables ...
251
+ Creating table bananas_banana
252
+ Installing custom SQL ...
253
+ Installing indexes ...
254
+ When I browse the list of bananas
255
+ Then I should see the text "No bananas left"
256
+
257
+ 2 features (2 passed)
258
+ 2 scenarios (2 passed)
259
+ 6 steps (6 passed)
260
+ ```
261
+
262
+ How neverfails works
263
+ ====================
264
+
265
+ With neverfails, all the steps are parsed by cucumber as normal. However, if a step fails, neverfails *does not* raise an AssertionError but runs the code to make the step pass, then runs the step again.
266
+
267
+ So far, neverfails is only able to recognize the three kinds of step included in the `grocery` sample project: creating a model, creating a view, adding text to that view. This is why I call neverfails a proof of concept. If other people find this project interesting (or if I get more time to work on this), then neverfails will grow up to the point where people with no programming experience will be able to create complex web applications by describing what they wish for.
268
+
269
+
270
+ Installing neverfails
271
+ =====================
272
+
273
+ To follow the example described above, you need [rails](https://github.com/rails/rails) and [bundler](http://gembundler.com) installed on your machine.
274
+ The following commands will install these packages, given you already have Ruby installed with [rubygems](http://rubygems.org) enabled:
275
+
276
+ ``` bash
277
+ gem install rails
278
+ gem install bundler
279
+ ```
280
+
281
+ The actual neverfails gem can either be downloaded from GitHub or installed by adding the following to the Rails project's Gemfile:
282
+
283
+ gem 'neverfails'
284
+
285
+ and the running:
286
+
287
+ bundle install
data/TODO.md ADDED
@@ -0,0 +1,9 @@
1
+ Bugs
2
+ ----
3
+
4
+ * Because of @@missing_view_file, the third match will fail if the second has not been executed in the same execution
5
+
6
+ Improvements
7
+ ------------
8
+
9
+ * Don't use class and global variables ($last_url, @@missing_view_file)
data/fails_steps.rb ADDED
@@ -0,0 +1,49 @@
1
+ # MODELS
2
+
3
+ Given /^there are no (\S+?)$/ do |objects|
4
+ model_name = objects.classify
5
+ Given "there is a model called #{model_name}"
6
+ Given "there are no instances of that model"
7
+ end
8
+
9
+ Given /^(?:|there is )a model called (.+?)$/ do |model_name|
10
+ assert ActiveRecord::Base.connection.tables.include?(model_name.tableize),
11
+ "No model found called #{model_name}"
12
+ @last_model = model_name.constantize
13
+ end
14
+
15
+ Given /^(?:|there are )no instances of that model$/ do
16
+ @last_model.delete_all
17
+ end
18
+
19
+ # NAVIGATION
20
+
21
+ When /^I browse the list of (.+?)$/ do |models|
22
+ Given "there is a page listing #{models}"
23
+ When "I navigate to that page"
24
+ end
25
+
26
+ Given /^there is a page listing (.+?)$/ do |models|
27
+ Given "there is a page with URL /#{models}/index"
28
+ end
29
+
30
+ Given /^there is a page with URL (.+?)$/ do |url|
31
+ assert Rails.application.routes.routes.collect(&:conditions).
32
+ collect{|route| route[:path_info] =~ url }.any?,
33
+ "No URL pattern found matching #{url}"
34
+ $last_url = url
35
+ end
36
+
37
+ When /^I navigate to that page$/ do
38
+ visit $last_url
39
+ end
40
+
41
+ # CONTENT
42
+
43
+ Then /^I should see the text "([^"]*)"$/ do |text|
44
+ begin
45
+ assert_contain text
46
+ rescue Test::Unit::AssertionFailedError => e
47
+ raise e.class, "The text \"#{text}\" was not found in the current page"
48
+ end
49
+ end
data/lib/neverfails.rb ADDED
@@ -0,0 +1,60 @@
1
+ require 'rails/generators'
2
+
3
+ module Cucumber
4
+ class StepMatch
5
+ def invoke_with_neverfails(multiline_arg)
6
+ begin
7
+ invoke_without_neverfails(multiline_arg)
8
+ rescue Exception => e # NOTE: Test::Unit::AssertionFailedError
9
+ match1 = /No model found called (.+?)\.$/.match(e.message)
10
+ match2 = /No URL pattern found matching \/(.+?)\.$/.match(e.message)
11
+ match3 = /The text "(.+?)" was not found in the current page$/.match(e.message)
12
+ if match1
13
+ create_missing_model match1[1]
14
+ elsif match2
15
+ create_missing_page_listing match2[1]
16
+ elsif match3
17
+ create_missing_text match3[1]
18
+ else
19
+ raise
20
+ end
21
+ return invoke_without_neverfails(multiline_arg) # Try again
22
+ end
23
+ end
24
+
25
+ alias_method_chain :invoke, :neverfails
26
+
27
+ end
28
+ end
29
+
30
+ def create_missing_model(singular_name)
31
+ # Generate the model and the migration
32
+ Rails::Generators.invoke("model", [singular_name, "--orm=active_record", "--migration"])
33
+ # Run the migration
34
+ ActiveRecord::Migrator.migrate "db/migrate/"
35
+ end
36
+
37
+ def create_missing_page_listing(objects)
38
+ # Generate the controller
39
+ require 'ruby-debug'
40
+ debugger
41
+ Rails::Generators.invoke("controller", [objects.classify, "index"])
42
+ # Add an extra route match "/models" => "model#index"
43
+ singular = objects.singularize
44
+ @@missing_view_file = "app/views/#{singular}/index.html.erb"
45
+ routes_file = 'config/routes.rb'
46
+ old_routes = File.read(routes_file)
47
+ File.open(routes_file, "w") do |file|
48
+ file.puts old_routes.gsub(/get "#{singular}\/index"/,
49
+ "get \"#{singular}/index\"\nmatch \"/#{objects}\" => \"#{singular}#index\"")
50
+ end
51
+ # Reload routes
52
+ ::Rails.application.reload_routes!
53
+ end
54
+
55
+ def create_missing_text(text)
56
+ File.open(@@missing_view_file, "w") do |file|
57
+ file.puts "#{text}\n"
58
+ end
59
+ Capybara.current_session.visit $last_url
60
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'neverfails'
6
+ s.version = '0.0.1'
7
+ s.authors = ["Claudio B."]
8
+ s.email = ["claudiob@gmail.com"]
9
+ s.homepage = "https://github.com/claudiob/neverfails/tree/rails"
10
+ s.summary = %q{Cucumber plugin that generates code to make failing steps pass}
11
+ s.description = %q{With neverfails, step definitions do not simply check whether the existing code satifies the required behaviour or not. They also write the code to make them pass.}
12
+ s.licenses = ["MIT"]
13
+
14
+ s.add_dependency 'cucumber'
15
+ s.add_dependency 'cucumber-rails'
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_path = "lib"
21
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: neverfails
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Claudio B.
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-08-07 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: cucumber
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: cucumber-rails
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ description: With neverfails, step definitions do not simply check whether the existing code satifies the required behaviour or not. They also write the code to make them pass.
50
+ email:
51
+ - claudiob@gmail.com
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - .gitignore
60
+ - Gemfile
61
+ - HISTORY.md
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - TODO.md
65
+ - fails_steps.rb
66
+ - lib/neverfails.rb
67
+ - neverfails.gemspec
68
+ has_rdoc: true
69
+ homepage: https://github.com/claudiob/neverfails/tree/rails
70
+ licenses:
71
+ - MIT
72
+ post_install_message:
73
+ rdoc_options: []
74
+
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ none: false
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ hash: 3
83
+ segments:
84
+ - 0
85
+ version: "0"
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ requirements: []
96
+
97
+ rubyforge_project:
98
+ rubygems_version: 1.3.7
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Cucumber plugin that generates code to make failing steps pass
102
+ test_files: []
103
+