coulda 0.3.1 → 0.4.0

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.
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
@@ -1,9 +1,9 @@
1
- coulda
1
+ Coulda
2
2
  ------
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.
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
- coulda, obviously was inspired by Cucumber, Shoulda, but also Thor. coulda believes that the best way to reuse
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
@@ -6,7 +6,7 @@ require 'rake/gempackagetask'
6
6
 
7
7
  require 'lib/coulda/tasks'
8
8
 
9
- gem 'thoughtbot-shoulda'
9
+ gem 'shoulda'
10
10
  require 'shoulda'
11
11
 
12
12
  # Test::Unit::UI::VERBOSE
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{coulda}
8
- s.version = "0.3.1"
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-09-23}
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 = [
@@ -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.for_name(name)
17
- f.class_eval(&block) if block_given?
18
- f.assert_description
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
- # Slightly tweeked version of #constantize from ActiveSupport 2.3.3
24
- class String
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'
@@ -1,119 +1,57 @@
1
1
  module Coulda
2
- Statement = Struct.new(:type, :name, :block)
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
- module FeatureBehavior
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
- def #{statement}(name, &block)
8
- @pending = true unless block_given?
9
- @statements << stmt = Statement.new(:#{statement}, name, block)
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
- private
73
-
74
- def create_test_method_for(name, &test_implementation)
75
- method_name = "test_#{name.sub(/\s+/, "_").downcase}"
76
- @scenarios ||= []
77
- if block_given?
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
- @scenarios << scenario
94
- scenario
95
- end
34
+ HERE
35
+ end
96
36
 
97
- def extract_statements_from(&test_implementation)
98
- @statements ||= []
99
- self.instance_eval(&test_implementation)
100
- statements = @statements
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
- # Scenario-less features should be pending
107
- def default_test
108
- pending
43
+ # Accessor to the Scenario-created TestCases
44
+ def tests
45
+ @scenarios.collect { |s| s.test_class }
109
46
  end
110
47
 
111
- %w[Given When Then And].each do |statement|
112
- eval <<-HERE
113
- def #{statement}(name, &block)
114
- yield if block_given?
115
- end
116
- HERE
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
@@ -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
- def initialize(name, &block)
7
- @name = name.to_s
8
- @block = block
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
- @pending = !block_given?
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
@@ -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 instance" do
5
- @@counter = 1
4
+ context "A Feature" do
6
5
  setup do
7
- feature_class = Feature.for_name "foobarblech#{@@counter}"
8
- @@counter += 1
9
- @feature = feature_class.new("default_test")
10
- end
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
- context "A Feature class" do
20
- should "have Test::Unit::TestCase as an ancestor" do
21
- assert(Feature.ancestors.include?(Test::Unit::TestCase))
14
+ should "contain tests" do
15
+ assert(@feature.tests.first)
22
16
  end
23
17
 
24
- context "created by name" do
25
- @@counter = 1
26
- setup do
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 errors when run" do
97
- result = run_feature @feature_without_scenarios
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 "pending 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 = :test_pending_scenario if RUBY_VERSION =~ /^1.9/
120
- assert(@feature_without_errors.instance_methods.include?(test_name), "Feature is missing test method from scenario")
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
- include GivenSomething
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
@@ -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.3.1
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-09-23 00:00:00 -04:00
12
+ date: 2009-11-23 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency