coulda 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +6 -0
- data/README.rdoc +14 -4
- data/Rakefile +1 -1
- data/coulda.gemspec +2 -2
- data/lib/coulda.rb +6 -50
- data/lib/coulda/feature.rb +41 -103
- data/lib/coulda/scenario.rb +63 -6
- data/test/feature_test.rb +17 -36
- data/test/integration/using_coulda_test.rb +1 -1
- data/test/regression/issue_1_test.rb +2 -2
- data/test/scenario_test.rb +12 -2
- metadata +2 -2
data/HISTORY
CHANGED
@@ -27,3 +27,9 @@ Cleanup
|
|
27
27
|
- Minor refactorings
|
28
28
|
- Added more examples
|
29
29
|
- Adds "And" support to Given, When, and Then
|
30
|
+
|
31
|
+
0.4.0
|
32
|
+
-----
|
33
|
+
- Complete internal redesign/rewrite
|
34
|
+
- Feature method now accepts a :testcase_class => Class argument specifying the subclass of Test::Unit::TestCase,
|
35
|
+
allowing for simple integration with other Test::Unit frameworks
|
data/README.rdoc
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
Coulda
|
2
2
|
------
|
3
|
-
|
4
|
-
no Gherkin/plain-text/external DSL.
|
3
|
+
Coulda gives you Cucumber-like BDD power but with an internal DSL. This means no "call-by-regexp" and
|
4
|
+
no Gherkin/plain-text/external DSL. Coulda is just code -- but with a Cucumber-like take on BDD.
|
5
5
|
|
6
|
-
|
6
|
+
Coulda, obviously was inspired by Cucumber, Shoulda, but also Thor. Coulda believes that the best way to reuse
|
7
7
|
code is the good ol' fashioned method. Instead of sharing files of regexps mapped to procs, you just write
|
8
8
|
methods. That simple.
|
9
9
|
|
@@ -11,6 +11,10 @@ You can define the implementation of your Given/When/Then right there with the b
|
|
11
11
|
if you want to reuse some Givens/Whens/Thens, just write a method! And if you find your Feature is getting
|
12
12
|
too bloated or that you would like to reuse Givens/Whens/Thens between features, put them in a helper.
|
13
13
|
|
14
|
+
Coulda also easily supports your Test::Unit framework of choice (including Rails Tests).
|
15
|
+
|
16
|
+
See http://coulda.tiggerpalace.com for more information.
|
17
|
+
|
14
18
|
Easy as pie.
|
15
19
|
|
16
20
|
require 'rubygems'
|
@@ -46,3 +50,9 @@ Easy as pie.
|
|
46
50
|
Then "blech" do; end
|
47
51
|
end
|
48
52
|
end
|
53
|
+
|
54
|
+
Thanks to
|
55
|
+
---------
|
56
|
+
* David Chelimsky and Jim Weirich for turning me into the test-obsessed developer that I am.
|
57
|
+
* Timothy King for putting up with my scotch-powered rambling while working on v0.4.0
|
58
|
+
* We Are Titans (http://www.wearetitans.net/) for sponsoring v0.4.0
|
data/Rakefile
CHANGED
data/coulda.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{coulda}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.4.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Evan David Light"]
|
12
|
-
s.date = %q{2009-
|
12
|
+
s.date = %q{2009-11-23}
|
13
13
|
s.description = %q{Behaviour Driven Development derived from Cucumber but as an internal DSL with methods for reuse}
|
14
14
|
s.email = %q{evan@tiggerpalace.com}
|
15
15
|
s.extra_rdoc_files = [
|
data/lib/coulda.rb
CHANGED
@@ -2,7 +2,6 @@ module Coulda
|
|
2
2
|
SyntaxError = Class.new(StandardError)
|
3
3
|
end
|
4
4
|
|
5
|
-
|
6
5
|
require 'test/unit'
|
7
6
|
|
8
7
|
gem 'jeremymcanally-pending', '>= 0.1'
|
@@ -12,56 +11,13 @@ require 'coulda/feature'
|
|
12
11
|
require 'coulda/scenario'
|
13
12
|
|
14
13
|
module Kernel
|
15
|
-
def Feature(name, &block)
|
16
|
-
f = Feature.
|
17
|
-
f.
|
18
|
-
f.
|
14
|
+
def Feature(name, opts = {}, &block)
|
15
|
+
f = Feature.new(name, opts)
|
16
|
+
f.instance_eval &block if block_given?
|
17
|
+
f.assert_presence_of_intent
|
19
18
|
f
|
20
19
|
end
|
21
20
|
end
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
# Ruby 1.9 introduces an inherit argument for Module#const_get and
|
26
|
-
# #const_defined? and changes their default behavior.
|
27
|
-
if Module.method(:const_get).arity == 1
|
28
|
-
# Tries to find a constant with the name specified in the argument string:
|
29
|
-
#
|
30
|
-
# "Module".constantize # => Module
|
31
|
-
# "Test::Unit".constantize # => Test::Unit
|
32
|
-
#
|
33
|
-
# The name is assumed to be the one of a top-level constant, no matter whether
|
34
|
-
# it starts with "::" or not. No lexical context is taken into account:
|
35
|
-
#
|
36
|
-
# C = 'outside'
|
37
|
-
# module M
|
38
|
-
# C = 'inside'
|
39
|
-
# C # => 'inside'
|
40
|
-
# "C".constantize # => 'outside', same as ::C
|
41
|
-
# end
|
42
|
-
#
|
43
|
-
# NameError is raised when the name is not in CamelCase or the constant is
|
44
|
-
# unknown.
|
45
|
-
def constantize
|
46
|
-
names = self.split('::')
|
47
|
-
names.shift if names.empty? || names.first.empty?
|
48
|
-
|
49
|
-
constant = Object
|
50
|
-
names.each do |name|
|
51
|
-
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
52
|
-
end
|
53
|
-
constant
|
54
|
-
end
|
55
|
-
else
|
56
|
-
def constantize #:nodoc:
|
57
|
-
names = self.split('::')
|
58
|
-
names.shift if names.empty? || names.first.empty?
|
59
|
-
|
60
|
-
constant = Object
|
61
|
-
names.each do |name|
|
62
|
-
constant = constant.const_get(name, false) || constant.const_missing(name)
|
63
|
-
end
|
64
|
-
constant
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
22
|
+
require 'vendor/constantize'
|
23
|
+
require 'vendor/underscore'
|
data/lib/coulda/feature.rb
CHANGED
@@ -1,119 +1,57 @@
|
|
1
1
|
module Coulda
|
2
|
-
|
2
|
+
# The Feature class is composed of an intent and a series of zero or more Scenarios that describe the behavior
|
3
|
+
# that satisfies the intent. Capturing intent is a key feature of Coulda. Intent defines the business value
|
4
|
+
# the Feature is attempting to fulfill. It serves as a reminder for developers (and customers) as to why this
|
5
|
+
# Feature was implemented and why this spec was written.
|
6
|
+
class Feature
|
7
|
+
include Test::Unit::Assertions
|
8
|
+
|
9
|
+
attr_reader :scenarios, :name
|
10
|
+
attr_accessor :current_scenario
|
11
|
+
|
12
|
+
def initialize(name, opts = {})
|
13
|
+
@name = name
|
14
|
+
@scenarios = []
|
15
|
+
Scenario.testcase_class = opts[:testcase_class] if opts[:testcase_class]
|
16
|
+
end
|
3
17
|
|
4
|
-
|
5
|
-
%w[Given When Then And].each do |statement|
|
18
|
+
%w[in_order_to as_a i_want_to].each do |intent|
|
6
19
|
eval <<-HERE
|
7
|
-
|
8
|
-
|
9
|
-
|
20
|
+
# An intent specifier
|
21
|
+
def #{intent}(val = nil)
|
22
|
+
val ? @#{intent} = val : @#{intent}
|
10
23
|
end
|
11
24
|
HERE
|
12
25
|
end
|
13
|
-
end
|
14
|
-
|
15
|
-
class Feature < Test::Unit::TestCase
|
16
|
-
class << self
|
17
|
-
attr_accessor :description
|
18
|
-
attr_reader :in_order_to, :as_a, :i_want_to
|
19
|
-
|
20
|
-
def include(*args)
|
21
|
-
super *args
|
22
|
-
extend *args
|
23
|
-
end
|
24
|
-
|
25
|
-
[:in_order_to, :as_a, :i_want_to].each do |field|
|
26
|
-
eval <<-HERE
|
27
|
-
def #{field}(arg = nil)
|
28
|
-
if arg
|
29
|
-
@#{field} = arg
|
30
|
-
else
|
31
|
-
@#{field}
|
32
|
-
end
|
33
|
-
end
|
34
|
-
HERE
|
35
|
-
end
|
36
|
-
|
37
|
-
def for_name(name)
|
38
|
-
klass = Class.new(Feature)
|
39
|
-
klass.extend(FeatureBehavior)
|
40
|
-
klass.description = name
|
41
|
-
class_name = feature_name_from(name)
|
42
|
-
Object.const_set(class_name, klass)
|
43
|
-
klass
|
44
|
-
end
|
45
|
-
|
46
|
-
def feature_name_from(name)
|
47
|
-
"Feature" + name.split(/\s|_/).map { |w| w.capitalize }.join
|
48
|
-
end
|
49
|
-
|
50
|
-
def assert_description
|
51
|
-
if @in_order_to || @as_a || @i_want_to
|
52
|
-
raise SyntaxError.new("Must call in_order_to if as_a and/or i_wanted_to called") unless @in_order_to
|
53
|
-
raise SyntaxError.new("Must call as_a if in_order_to and/or i_want_to called") unless @as_a
|
54
|
-
raise SyntaxError.new("Must call i_want_to if in_order_to and/or as_a called") unless @i_want_to
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def Scenario(name, &test_implementation)
|
59
|
-
raise SyntaxError.new("A scenario requires a name") unless name
|
60
|
-
create_test_method_for(name, &test_implementation)
|
61
|
-
create_scenario_for(name, &test_implementation)
|
62
|
-
end
|
63
|
-
|
64
|
-
def scenarios
|
65
|
-
@scenarios ||= []
|
66
|
-
end
|
67
|
-
|
68
|
-
def pending?
|
69
|
-
@scenarios.all? { |s| !s.pending? }
|
70
|
-
end
|
71
26
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
define_method(method_name, &test_implementation)
|
79
|
-
else
|
80
|
-
define_method(method_name) { pending }
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def create_scenario_for(name, &test_implementation)
|
85
|
-
@scenarios ||= []
|
86
|
-
scenario = nil
|
87
|
-
if block_given?
|
88
|
-
scenario = Scenario.new(name, &test_implementation)
|
89
|
-
scenario.statements = extract_statements_from &test_implementation
|
90
|
-
else
|
91
|
-
scenario = Scenario.new(name)
|
27
|
+
%w[Given When Then And].each do |stmt|
|
28
|
+
eval <<-HERE
|
29
|
+
# Specifies a prereqisite, event, or expectation. May be used in any order within the spec
|
30
|
+
def #{stmt}(text, &block)
|
31
|
+
@current_scenario.statements << { :type => :#{stmt}, :text => text }
|
32
|
+
@current_scenario.steps << block if block
|
92
33
|
end
|
93
|
-
|
94
|
-
|
95
|
-
end
|
34
|
+
HERE
|
35
|
+
end
|
96
36
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
@statements = []
|
102
|
-
statements
|
103
|
-
end
|
37
|
+
# Creates a Scenario instance and adds it to the Feature
|
38
|
+
def Scenario(scenario_name, &block)
|
39
|
+
@scenarios << scenario = Scenario.new(scenario_name, self, &block)
|
40
|
+
scenario
|
104
41
|
end
|
105
42
|
|
106
|
-
#
|
107
|
-
def
|
108
|
-
|
43
|
+
# Accessor to the Scenario-created TestCases
|
44
|
+
def tests
|
45
|
+
@scenarios.collect { |s| s.test_class }
|
109
46
|
end
|
110
47
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
48
|
+
|
49
|
+
# Raises an error if only some of the intent is captured (i.e., in_order_to and as_a without i_want_to)
|
50
|
+
def assert_presence_of_intent
|
51
|
+
presence = %w[in_order_to as_a i_want_to].map { |intent| instance_variable_get("@#{intent}") }
|
52
|
+
if presence.any? { |p| p } && !presence.all? { |p| p }
|
53
|
+
raise SyntaxError.new("Must have all or none of in_order, as_a, and i_want_to called in a Feature")
|
54
|
+
end
|
117
55
|
end
|
118
56
|
end
|
119
57
|
end
|
data/lib/coulda/scenario.rb
CHANGED
@@ -1,17 +1,74 @@
|
|
1
1
|
module Coulda
|
2
|
+
# A factory for Test::Unit::TestCases (or their subclass).
|
2
3
|
class Scenario
|
3
|
-
attr_reader :name
|
4
|
-
attr_accessor :statements
|
4
|
+
attr_reader :name, :test_class
|
5
|
+
attr_accessor :steps, :statements
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
# Class-level method to set the subclass of Test::Unit::TestCase to use as the parent for
|
8
|
+
# tests manufactured by Scenarios.
|
9
|
+
def self.testcase_class=(klass)
|
10
|
+
unless klass.ancestors.include?(Test::Unit::TestCase)
|
11
|
+
raise Exception.new("class must inherit from Test::Unit:TestCase")
|
12
|
+
end
|
13
|
+
@testcase_class = klass
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.testcase_class
|
17
|
+
@testcase_class
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(name, my_feature, &block)
|
21
|
+
raise Exception.new("Scenario must have a name") unless name
|
22
|
+
@name = name
|
9
23
|
@statements = []
|
10
|
-
@
|
24
|
+
@steps = []
|
25
|
+
@pending = false
|
26
|
+
@test_class = ::Class.new(Scenario.testcase_class || Test::Unit::TestCase)
|
27
|
+
assign_test_class_to_const(my_feature)
|
28
|
+
test_impl = nil
|
29
|
+
if block
|
30
|
+
create_and_provision_test_method_for my_feature, &block
|
31
|
+
else
|
32
|
+
@pending = true
|
33
|
+
define_test_method_using { pending }
|
34
|
+
end
|
11
35
|
end
|
12
36
|
|
37
|
+
# Predicate indicating if the Scenario was provided with an example
|
13
38
|
def pending?
|
14
39
|
@pending
|
15
40
|
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def create_and_provision_test_method_for(feature, &block)
|
45
|
+
execute block, :within => feature
|
46
|
+
inject_test_steps_into @test_class
|
47
|
+
define_test_method_using { self.class.test_steps.each { |s| feature.instance_eval &s } }
|
48
|
+
end
|
49
|
+
|
50
|
+
def assign_test_class_to_const(my_feature)
|
51
|
+
base_name = "#{my_feature.name}_#{@name}_#{rand(1_000_000_000)}"
|
52
|
+
base_name = "letter_" + base_name if base_name =~ /^[^a-zA-Z]/
|
53
|
+
titleized_underscored_name = base_name.super_custom_underscore.gsub(/\b('?[a-z])/) { $1.upcase }
|
54
|
+
::Module.const_set(titleized_underscored_name, @test_class)
|
55
|
+
end
|
56
|
+
|
57
|
+
def define_test_method_using(&block)
|
58
|
+
@test_class.send(:define_method, "test_#{@name.downcase.super_custom_underscore}", &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def execute(block, params = {})
|
62
|
+
feature = params[:within]
|
63
|
+
feature.current_scenario = self
|
64
|
+
feature.instance_eval &block
|
65
|
+
end
|
66
|
+
|
67
|
+
def inject_test_steps_into(test_class)
|
68
|
+
class << test_class
|
69
|
+
attr_accessor :test_steps
|
70
|
+
end
|
71
|
+
test_class.test_steps = @steps
|
72
|
+
end
|
16
73
|
end
|
17
74
|
end
|
data/test/feature_test.rb
CHANGED
@@ -1,41 +1,23 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), "test_helper")
|
2
2
|
|
3
3
|
class FeatureTest < Test::Unit::TestCase
|
4
|
-
context "A Feature
|
5
|
-
@@counter = 1
|
4
|
+
context "A Feature" do
|
6
5
|
setup do
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
%w[Given When Then And].each do |condition|
|
13
|
-
should "have a method called '#{condition}'" do
|
14
|
-
assert(@feature.respond_to?(condition))
|
6
|
+
@feature = Feature "foobarblech#{@@counter}" do
|
7
|
+
Scenario "" do
|
8
|
+
Given "" do; end
|
9
|
+
Then "" do; end
|
10
|
+
end
|
15
11
|
end
|
16
12
|
end
|
17
|
-
end
|
18
13
|
|
19
|
-
|
20
|
-
|
21
|
-
assert(Feature.ancestors.include?(Test::Unit::TestCase))
|
14
|
+
should "contain tests" do
|
15
|
+
assert(@feature.tests.first)
|
22
16
|
end
|
23
17
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@feature = Feature.for_name "foo#{@@counter}"
|
28
|
-
@@counter += 1
|
29
|
-
end
|
30
|
-
|
31
|
-
should "be a subclass of Feature" do
|
32
|
-
assert(@feature.ancestors.include?(Feature))
|
33
|
-
end
|
34
|
-
|
35
|
-
%w[Given When Then And].each do |condition|
|
36
|
-
should "have a method called '#{condition}'" do
|
37
|
-
assert(@feature.respond_to?(condition), "does not have a method called #{condition}")
|
38
|
-
end
|
18
|
+
%w[Given When Then And].each do |condition|
|
19
|
+
should "have a method called '#{condition}'" do
|
20
|
+
assert(@feature.respond_to?(condition))
|
39
21
|
end
|
40
22
|
end
|
41
23
|
|
@@ -93,9 +75,8 @@ class FeatureTest < Test::Unit::TestCase
|
|
93
75
|
end
|
94
76
|
end
|
95
77
|
|
96
|
-
should "not have any
|
97
|
-
|
98
|
-
Object::RUBY_VERSION =~ /^1.9/ ? assert(result) : assert(result.passed?)
|
78
|
+
should "not have any test" do
|
79
|
+
assert(@feature_without_scenarios.tests.empty?)
|
99
80
|
end
|
100
81
|
end
|
101
82
|
|
@@ -113,11 +94,11 @@ class FeatureTest < Test::Unit::TestCase
|
|
113
94
|
### Integration tests
|
114
95
|
|
115
96
|
context "with a block containing a scenario" do
|
116
|
-
should "create a Feature instance method named 'test_<underscored_scenario_name>'" do
|
117
|
-
@feature_without_errors.Scenario
|
97
|
+
should "create a Feature instance method named 'test_<underscored_feature_name>_<underscored_scenario_name>'" do
|
98
|
+
@feature_without_errors.Scenario("pending scenario") {}
|
118
99
|
test_name = "test_pending_scenario"
|
119
|
-
test_name =
|
120
|
-
assert(@feature_without_errors.instance_methods.include?(test_name), "
|
100
|
+
test_name = test_name.to_sym if RUBY_VERSION =~ /^1.9/
|
101
|
+
assert(@feature_without_errors.tests.first.instance_methods.include?(test_name), "Test is missing test method from scenario")
|
121
102
|
end
|
122
103
|
|
123
104
|
should "create a Scenario" do
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), "..", "test_helper")
|
2
2
|
|
3
|
-
Feature "Using Coulda" do
|
3
|
+
Feature "Using Coulda", :testcase_class => Test::Unit::TestCase do
|
4
4
|
in_order_to "perform lightweight integration/acceptance testing with Coulda"
|
5
5
|
as_a "developer"
|
6
6
|
i_want_to "have typical Coulda usage work"
|
@@ -13,7 +13,7 @@ class Issue1Test < Test::Unit::TestCase
|
|
13
13
|
pendings_are_errors
|
14
14
|
|
15
15
|
@feature_without_errors = Feature "Issue 1 feature" do
|
16
|
-
|
16
|
+
extend GivenSomething
|
17
17
|
|
18
18
|
in_order_to "foo"
|
19
19
|
as_a "bar"
|
@@ -28,7 +28,7 @@ class Issue1Test < Test::Unit::TestCase
|
|
28
28
|
end
|
29
29
|
|
30
30
|
should "pass" do
|
31
|
-
assert(run_feature(@feature_without_errors))
|
31
|
+
assert(run_feature(@feature_without_errors.tests.first))
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
data/test/scenario_test.rb
CHANGED
@@ -3,13 +3,13 @@ require File.join(File.dirname(__FILE__), "test_helper")
|
|
3
3
|
class ScenarioTest < Test::Unit::TestCase
|
4
4
|
context "A Scenario" do
|
5
5
|
setup do
|
6
|
-
@scenario = Scenario.new("foobar")
|
6
|
+
@scenario = Scenario.new("foobar", Feature("something_or_other")) {}
|
7
7
|
end
|
8
8
|
|
9
9
|
context "when instantiated" do
|
10
10
|
context "with only a String" do
|
11
11
|
setup do
|
12
|
-
@scenario = Scenario.new("foobar")
|
12
|
+
@scenario = Scenario.new("foobar", Feature("another"))
|
13
13
|
end
|
14
14
|
|
15
15
|
should "be pending" do
|
@@ -18,5 +18,15 @@ class ScenarioTest < Test::Unit::TestCase
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
21
|
+
|
22
|
+
should "allow Test::Unit::TestCase class to subclass to be set" do
|
23
|
+
Scenario.testcase_class = Test::Unit::TestCase
|
24
|
+
end
|
25
|
+
|
26
|
+
should "raise an Exception if provided a class that does not inherit from Test::Unit::TestCase" do
|
27
|
+
assert_raises Exception do
|
28
|
+
Scenario.testcase_class = Object
|
29
|
+
end
|
30
|
+
end
|
21
31
|
end
|
22
32
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: coulda
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evan David Light
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-11-23 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|