hypercuke 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +142 -0
- data/Rakefile +11 -0
- data/bin/hcu +19 -0
- data/hypercuke.gemspec +28 -0
- data/lib/hypercuke.rb +43 -0
- data/lib/hypercuke/adapter_definition.rb +29 -0
- data/lib/hypercuke/cli.rb +53 -0
- data/lib/hypercuke/cli/builder.rb +68 -0
- data/lib/hypercuke/cli/parser.rb +76 -0
- data/lib/hypercuke/config.rb +20 -0
- data/lib/hypercuke/context.rb +36 -0
- data/lib/hypercuke/cucumber_integration.rb +15 -0
- data/lib/hypercuke/exceptions.rb +42 -0
- data/lib/hypercuke/mini_inflector.rb +9 -0
- data/lib/hypercuke/name_list.rb +38 -0
- data/lib/hypercuke/step_adapter.rb +12 -0
- data/lib/hypercuke/step_adapters.rb +126 -0
- data/lib/hypercuke/step_driver.rb +52 -0
- data/lib/hypercuke/version.rb +3 -0
- data/spec/cli_spec.rb +135 -0
- data/spec/context_spec.rb +46 -0
- data/spec/hypercuke_spec.rb +29 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/step_adapter_definition_spec.rb +131 -0
- data/spec/step_driver_spec.rb +99 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2df7dbaea6ea4f5665a5f0e698e677f27fa3d9fd
|
4
|
+
data.tar.gz: 1334aa38a72f29866cff7c844808b59454809a00
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8e8ac55bd1d0e6773d148aa7e949904a1c90b171ce6b01e26e48dc40969386b46e317a69fc8ea2f5fe0922304576884ce99dd9ef334fcc289b2edddf2e8c0111
|
7
|
+
data.tar.gz: ee444352aaef8664bd45206b3485c60402cbb31754033446d8f9526805b1e1d9597952383c7267ef4b5a4feb450ee9fed452542ee76c4ed2308b32f7f1afa7f2
|
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
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
|
+
private_tasks.rake
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color --order random
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
hypercuke
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-1.9.3-p484
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Sam Livingston-Gray
|
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,142 @@
|
|
1
|
+
# Hypercuke
|
2
|
+
|
3
|
+
Hypercuke helps you use Cucumber to do BDD at multiple layers of your
|
4
|
+
application, and gently nudges you into writing your scenarios in
|
5
|
+
high-level terms that your users can understand.
|
6
|
+
|
7
|
+
## Why?
|
8
|
+
|
9
|
+
Because TATFT.
|
10
|
+
|
11
|
+
### Okay, But Why Cucumber?
|
12
|
+
|
13
|
+
Cucumber is a great way to write acceptance tests that are also, by
|
14
|
+
definition, integration tests. Each scenario defines a "walking
|
15
|
+
skeleton" (I've also heard "tracer bullet") -- one complete path through
|
16
|
+
the system from top to bottom, describing one feature that an end user
|
17
|
+
actually cares about.
|
18
|
+
|
19
|
+
### So Why Doesn't Everyone Do This Already?
|
20
|
+
|
21
|
+
The traditional Rails approach of using Cucumber to test an app from a
|
22
|
+
browser has some pitfalls:
|
23
|
+
|
24
|
+
* In the short term, sometimes there's a LOT of new functionality to
|
25
|
+
define, and you spend days or weeks just getting one scenario to pass.
|
26
|
+
This can suck the morale right out of you.
|
27
|
+
|
28
|
+
More importantly, Cucumber has some long-term pitfalls:
|
29
|
+
|
30
|
+
* Having all your tests fire up a web browser and exercise the web
|
31
|
+
application gets really really slow (especially when every! single!
|
32
|
+
test! starts with "Given I am logged in as a normal user").
|
33
|
+
|
34
|
+
* More subtly, the implicit assumption that "Cucumber ==> web browser"
|
35
|
+
makes it too easy for knowledge of the web UI to creep into tests --
|
36
|
+
and, as the Cucumber community has already learned (see <a
|
37
|
+
href="http://aslakhellesoy.com/post/11055981222/the-training-wheels-came-off">The
|
38
|
+
Training Wheels Came Off</a>), this can lead to slow, brittle tests.
|
39
|
+
|
40
|
+
* Cucumber tutorials tend to show you step definitions that combine a
|
41
|
+
regular expression with an associated block of code. Then they say
|
42
|
+
"TADA!" and wander off, leaving users with the impression that *that's
|
43
|
+
where all their code is supposed to go*. Because everything is in a
|
44
|
+
flat namespace, this tends to turn into... well, PHP. I've seen
|
45
|
+
projects with thousands of lines of horrible procedural code in deeply
|
46
|
+
interdependent step definitions that resist refactoring. In one
|
47
|
+
particularly memorable instance, I actually helped write a 50-line
|
48
|
+
step definitions with a parameter named "destroy_the_earth".
|
49
|
+
|
50
|
+
### Why Hypercuke?
|
51
|
+
|
52
|
+
Hypercuke's core idea is a clever way of using Cucumber tags. By
|
53
|
+
swapping out different adapters for your step definitions, you can write
|
54
|
+
a scenario once, tag it appropriately, and then execute that scenario to
|
55
|
+
test any or all of:
|
56
|
+
|
57
|
+
* a fast core layer of plain old Ruby objects,
|
58
|
+
* ActiveRecord models,
|
59
|
+
* some <a
|
60
|
+
href="http://brewhouse.io/blog/2014/04/30/gourmet-service-objects.html">Gourmet
|
61
|
+
Service Objects</a>,
|
62
|
+
* an API if you have one,
|
63
|
+
* the UI,
|
64
|
+
* or any other layer that's meaningful to you.
|
65
|
+
|
66
|
+
Hypercuke directly addresses each of the pain points described above:
|
67
|
+
|
68
|
+
* By starting off at a low layer, you can use your Cucumber scenario as
|
69
|
+
a short-span integration test that's just wrapped around a few simple
|
70
|
+
objects. Once you're satisfied with how that works, you can move up
|
71
|
+
to a higher level of abstraction. If a scenario is a "walking
|
72
|
+
skeleton", Hypercuke lets you start by building the skeleton just up
|
73
|
+
to the knees, then up to the spine, and so on.
|
74
|
+
|
75
|
+
* Just because your tests are in Cucumber doesn't mean they have to be
|
76
|
+
slow. I originally developed Cucumber because I wanted to describe
|
77
|
+
all of my features using Gherkin, but only test one or two scenarios
|
78
|
+
through a web browser. Scenarios to describe boundary cases,
|
79
|
+
exceptions, or variations can run against a lower layer of the
|
80
|
+
application, which can have as much or as little overhead as makes
|
81
|
+
sense for each scenario.
|
82
|
+
|
83
|
+
* Because my scenarios might run at varying levels of abstraction, I
|
84
|
+
write them in interface-agnostic language. (For example, I'll write
|
85
|
+
"I view the list of widgets" instead of "I go to /widgets".) And if I
|
86
|
+
forget, the cognitive dissonance when I write the step definitions
|
87
|
+
very quickly reminds me to use more generic language. This helps me
|
88
|
+
write tests at a high level of abstraction, and it also helps keep me
|
89
|
+
focused on *why* I'm writing this feature, so I don't get lost
|
90
|
+
building a gold-plated automated yak-shaving factory.
|
91
|
+
|
92
|
+
* Finally, Hypercuke provides *just enough* structure for you to write
|
93
|
+
reusable step definitions. Inside the regular-expression-plus-block
|
94
|
+
that Cucumber gives you, you write the bare minimum amount of code you
|
95
|
+
need to translate from Gherkin into a Ruby message, and then you send
|
96
|
+
that message to a step adapter that does the work. Step adapters are
|
97
|
+
Ruby objects, which means you can use all of your Ruby fu to keep your
|
98
|
+
code organized.
|
99
|
+
|
100
|
+
## How?
|
101
|
+
|
102
|
+
TODO: continue here :D
|
103
|
+
|
104
|
+
## About the Name
|
105
|
+
|
106
|
+
I started out with the concept of "layers", so this gem was originally
|
107
|
+
going to be called "cucumber-parfait". But as I worked through it, I
|
108
|
+
kept visualizing things using two-dimensional matrices, which kept
|
109
|
+
moving around in my brain as I thought about them... and that reminded
|
110
|
+
me of visualizations of a hypercube. Ergo, Hypercuke.
|
111
|
+
|
112
|
+
## Installation
|
113
|
+
|
114
|
+
Add this line to your application's Gemfile:
|
115
|
+
|
116
|
+
gem 'hypercuke'
|
117
|
+
|
118
|
+
And then execute:
|
119
|
+
|
120
|
+
$ bundle
|
121
|
+
|
122
|
+
Or install it yourself as:
|
123
|
+
|
124
|
+
$ gem install hypercuke
|
125
|
+
|
126
|
+
## Usage
|
127
|
+
|
128
|
+
Obviously I have some more writing to do, but you'll need to add this
|
129
|
+
line somewhere in your application's Cucumber environment (this is
|
130
|
+
usually somewhere in /features/support/):
|
131
|
+
|
132
|
+
require 'hypercuke/cucumber_integration'
|
133
|
+
|
134
|
+
TODO: Write more detailed usage instructions
|
135
|
+
|
136
|
+
## Contributing
|
137
|
+
|
138
|
+
1. Fork it ( https://github.com/[my-github-username]/hypercuke/fork )
|
139
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
140
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
141
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
142
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/hcu
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Make sure we can get to hypercuke on the load path
|
4
|
+
unless $:.any? {|path| path =~ /hypercuke\/lib/ }
|
5
|
+
lib_path = File.expand_path( File.join( File.dirname(__FILE__), *%w[.. lib] ) )
|
6
|
+
$:.unshift(lib_path)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Load the gem
|
10
|
+
begin
|
11
|
+
require 'hypercuke'
|
12
|
+
rescue LoadError
|
13
|
+
require 'rubygems' # worth a shot before just dying
|
14
|
+
require 'hypercuke'
|
15
|
+
end
|
16
|
+
|
17
|
+
# Hand the Hypercuke command off to the CLI, which will parse and
|
18
|
+
# Kernel#exec the appropriate Cucumber command
|
19
|
+
Hypercuke::CLI.exec ARGV, output_to: STDOUT
|
data/hypercuke.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hypercuke/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hypercuke"
|
8
|
+
spec.version = Hypercuke::VERSION
|
9
|
+
spec.authors = ["Sam Livingston-Gray"]
|
10
|
+
spec.email = ["sam.livingstongray@livingsocial.com"]
|
11
|
+
spec.summary = %q{Run Cucumber scenarios at multiple layers of your application.}
|
12
|
+
spec.description = %q{Hypercuke helps you use Cucumber to do BDD at multiple layers of your application, and gently nudges you into writing your scenarios in high-level terms that your users can understand.}
|
13
|
+
spec.homepage = "https://github.com/livingsocial/hypercuke"
|
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_runtime_dependency "cucumber", "~> 1.3"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0.0"
|
26
|
+
|
27
|
+
spec.add_development_dependency 'geminabox' # TODO: remove this when publishing
|
28
|
+
end
|
data/lib/hypercuke.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'hypercuke/version'
|
2
|
+
require 'hypercuke/cli'
|
3
|
+
require 'hypercuke/context'
|
4
|
+
require 'hypercuke/config'
|
5
|
+
require 'hypercuke/exceptions'
|
6
|
+
require 'hypercuke/mini_inflector'
|
7
|
+
require 'hypercuke/name_list'
|
8
|
+
require 'hypercuke/step_driver'
|
9
|
+
require 'hypercuke/step_adapter'
|
10
|
+
require 'hypercuke/step_adapters'
|
11
|
+
require 'hypercuke/adapter_definition'
|
12
|
+
|
13
|
+
module Hypercuke
|
14
|
+
LAYER_NAME_ENV_VAR = 'HYPERCUKE_LAYER'
|
15
|
+
|
16
|
+
def self.reset!
|
17
|
+
@config = nil
|
18
|
+
@current_layer = nil
|
19
|
+
StepAdapters.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.current_layer=(layer_name)
|
23
|
+
@current_layer = layer_name ? layer_name.to_sym : nil
|
24
|
+
end
|
25
|
+
def self.current_layer
|
26
|
+
layer_name = (@current_layer || ENV[LAYER_NAME_ENV_VAR])
|
27
|
+
layer_name && layer_name.to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def self.config
|
32
|
+
@config ||= Config.new
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
extend Forwardable
|
37
|
+
def_delegators :config, *[
|
38
|
+
:layers, :layer_names,
|
39
|
+
:topics, :topic_names,
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hypercuke
|
2
|
+
# Entry point for the adapter definition API
|
3
|
+
def self.topic(topic_name, &block)
|
4
|
+
AdapterDefinition.topic topic_name, &block
|
5
|
+
end
|
6
|
+
|
7
|
+
module AdapterDefinition
|
8
|
+
def self.topic(topic_name, &block)
|
9
|
+
Hypercuke.topics.define topic_name
|
10
|
+
tb = TopicBuilder.new(topic_name)
|
11
|
+
tb.instance_eval &block if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
class TopicBuilder
|
15
|
+
attr_reader :topic_name
|
16
|
+
def initialize(topic_name)
|
17
|
+
@topic_name = topic_name.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
# I know the name *says* "layer", but what it *means* is that we
|
21
|
+
# should define a step adapter for that layer.
|
22
|
+
def layer(layer_name, &block)
|
23
|
+
Hypercuke.layers.define layer_name
|
24
|
+
klass = Hypercuke::StepAdapters.define( topic_name, layer_name )
|
25
|
+
klass.module_eval &block if block_given?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'hypercuke/cli/parser'
|
2
|
+
require 'hypercuke/cli/builder'
|
3
|
+
|
4
|
+
module Hypercuke
|
5
|
+
class CLI
|
6
|
+
def self.exec(argv, opts = {})
|
7
|
+
cli = new(argv, opts[:output_to])
|
8
|
+
cli.run!
|
9
|
+
end
|
10
|
+
|
11
|
+
# NB: .bundler_present? is not covered by tests, because I can't
|
12
|
+
# think of a reasonable way to test it. PRs welcome. :)
|
13
|
+
def self.bundler_present?
|
14
|
+
!! (`which bundle` =~ /\wbundle\w/) # parens are significant
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(argv, output = nil, environment = ENV, kernel = Kernel)
|
18
|
+
@argv = argv
|
19
|
+
@output = output
|
20
|
+
@environment = environment
|
21
|
+
@kernel = kernel
|
22
|
+
end
|
23
|
+
|
24
|
+
def run!
|
25
|
+
output && output.puts(cucumber_command_for_display)
|
26
|
+
new_env = environment.to_hash.merge({ Hypercuke::LAYER_NAME_ENV_VAR => layer_name })
|
27
|
+
kernel.exec new_env, cucumber_command
|
28
|
+
end
|
29
|
+
|
30
|
+
def layer_name
|
31
|
+
parser.layer_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def cucumber_command
|
35
|
+
builder.cucumber_command_line(self.class.bundler_present?)
|
36
|
+
end
|
37
|
+
|
38
|
+
def cucumber_command_for_display
|
39
|
+
"HYPERCUKE_LAYER=#{layer_name} #{cucumber_command}"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
attr_reader :argv, :output, :environment, :kernel
|
44
|
+
|
45
|
+
def parser
|
46
|
+
@parser ||= Hypercuke::CLI::Parser.new(argv)
|
47
|
+
end
|
48
|
+
|
49
|
+
def builder
|
50
|
+
@builder ||= Hypercuke::CLI::Builder.new(parser.options)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Hypercuke
|
2
|
+
class CLI
|
3
|
+
|
4
|
+
# I take information extracted the Parser and use it to build a
|
5
|
+
# 'cucumber' command line
|
6
|
+
class Builder
|
7
|
+
def initialize(options)
|
8
|
+
@options = options
|
9
|
+
@cuke_args = []
|
10
|
+
build_cuke_args
|
11
|
+
end
|
12
|
+
|
13
|
+
def cucumber_command_line(prepend_bundler = false)
|
14
|
+
cmd = prepend_bundler ? 'bundle exec ' : ''
|
15
|
+
cmd << cuke_args.join(' ')
|
16
|
+
cmd
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
attr_reader :options, :cuke_args
|
21
|
+
|
22
|
+
def build_cuke_args
|
23
|
+
add_base_command
|
24
|
+
add_layer_tag_for_mode
|
25
|
+
add_profile_unless_already_present
|
26
|
+
pass_through_all_other_args
|
27
|
+
end
|
28
|
+
|
29
|
+
def add_base_command
|
30
|
+
cuke_args << 'cucumber'
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_layer_tag_for_mode
|
34
|
+
cuke_args << "--tags #{layer_tag_for_mode}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def layer_tag_for_mode
|
38
|
+
layer = options[:layer_name]
|
39
|
+
mode = options[:mode] || 'ok'
|
40
|
+
'@%s_%s' % [ layer, mode ]
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_profile_unless_already_present
|
44
|
+
if profile_specified?
|
45
|
+
add_profile options[:profile]
|
46
|
+
else
|
47
|
+
if options[:mode] == 'wip'
|
48
|
+
add_profile 'wip'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def profile_specified?
|
54
|
+
options[:profile].to_s !~ /^\s*$/
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_profile(profile_name)
|
58
|
+
cuke_args << '--profile'
|
59
|
+
cuke_args << profile_name
|
60
|
+
end
|
61
|
+
|
62
|
+
def pass_through_all_other_args
|
63
|
+
cuke_args.concat( options[:other_args] )
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|