rbehave 0.1.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.
@@ -0,0 +1,259 @@
1
+ require 'behaviour/examples/helper'
2
+
3
+ module RBehave
4
+ describe World do
5
+ setup do
6
+ World.listeners.clear
7
+ RBehave::Runner.dry_run = false
8
+ end
9
+
10
+ teardown do
11
+ World.listeners.clear
12
+ RBehave::Runner.dry_run = false
13
+ end
14
+
15
+ it 'should create an object that mixes in a World' do
16
+ # when
17
+ obj = World::create
18
+
19
+ # then
20
+ obj.should be_kind_of(World)
21
+ end
22
+
23
+ it 'should create an object of a specified type that mixes in a world' do
24
+ # when
25
+ obj = World::create String
26
+
27
+ # then
28
+ obj.should be_kind_of(String)
29
+ obj.should be_kind_of(World)
30
+ end
31
+
32
+ def ensure_world_executes_step(&block)
33
+ # given
34
+ obj = World::create
35
+ $step_ran = false
36
+
37
+ # when
38
+ obj.instance_eval(&block)
39
+
40
+ # then
41
+ $step_ran.should be_true
42
+ end
43
+
44
+ it 'should execute a Given, When or Then step' do
45
+ ensure_world_executes_step do
46
+ Given 'a given' do
47
+ $step_ran = true
48
+ end
49
+ end
50
+
51
+ ensure_world_executes_step do
52
+ When 'an event' do
53
+ $step_ran = true
54
+ end
55
+ end
56
+
57
+ ensure_world_executes_step do
58
+ Then 'an outcome' do
59
+ $step_ran = true
60
+ end
61
+ end
62
+ end
63
+
64
+ it 'should reuse a given across scenarios' do
65
+ # given
66
+ $num_invoked = 0
67
+ a_world = World::create
68
+ a_world.instance_eval do
69
+ Given 'a given' do
70
+ $num_invoked += 1
71
+ end
72
+ end
73
+ another_world = World::create
74
+
75
+ # when
76
+ another_world.instance_eval do
77
+ Given 'a given' # without a body
78
+ end
79
+
80
+ # then
81
+ $num_invoked.should == 2
82
+ end
83
+
84
+ it 'should reuse an event across scenarios' do
85
+ # given
86
+ $num_invoked = 0
87
+ a_world = World::create
88
+ a_world.instance_eval do
89
+ When 'an event' do
90
+ $num_invoked += 1
91
+ end
92
+ end
93
+
94
+ another_world = World::create
95
+
96
+ # when
97
+ another_world.instance_eval do
98
+ When 'an event' # without a body
99
+ end
100
+
101
+ # then
102
+ $num_invoked.should == 2
103
+ end
104
+
105
+ it 'should reuse an outcome across scenarios' do
106
+ # given
107
+ $num_invoked = 0
108
+ a_world = World::create
109
+ a_world.instance_eval do
110
+ Then 'an outcome' do
111
+ $num_invoked += 1
112
+ end
113
+ end
114
+
115
+ another_world = World::create
116
+
117
+ # when
118
+ another_world.instance_eval do
119
+ Then 'an outcome' # without a body
120
+ end
121
+
122
+ # then
123
+ $num_invoked.should == 2
124
+ end
125
+
126
+ it 'should preserve instance variables between steps within a scenario' do
127
+ # given
128
+ world = World::create
129
+ $first = nil
130
+ $second = nil
131
+
132
+ # when
133
+ world.instance_eval do
134
+ Given 'given' do
135
+ @first = 'first'
136
+ end
137
+ When 'event' do
138
+ @second = @first # from given
139
+ end
140
+ Then 'outcome' do
141
+ $first = @first # from given
142
+ $second = @second # from event
143
+ end
144
+ end
145
+
146
+ # then
147
+ ensure_that $first, is('first')
148
+ ensure_that $second, is('first')
149
+ end
150
+
151
+ it 'should invoke the reused step in the new object instance' do
152
+ # given
153
+ $instances = []
154
+ world1 = World.create
155
+ world1.instance_eval do
156
+ Given 'a given' do
157
+ $instances << self
158
+ end
159
+ end
160
+ world2 = World.create
161
+
162
+ # when
163
+ world2.instance_eval do
164
+ Given 'a given' # reused
165
+ Then 'an outcome' do
166
+ $instances << self
167
+ end
168
+ end
169
+
170
+ # then
171
+ $instances.should == [ world1, world2, world2 ]
172
+ end
173
+
174
+ def ensure_world_propagates_error(expected_error, &block)
175
+ # given
176
+ world = World.create
177
+ $error = nil
178
+
179
+ # when
180
+ error = exception_from do
181
+ world.instance_eval(&block)
182
+ end
183
+
184
+ # then
185
+ error.should be_kind_of(expected_error)
186
+ end
187
+
188
+ it 'should propagate a failure from a Given, When or Then step' do
189
+ ensure_world_propagates_error RuntimeError do
190
+ Given 'a given' do
191
+ raise RuntimeError, "oops"
192
+ end
193
+ end
194
+
195
+ ensure_world_propagates_error RuntimeError do
196
+ When 'an event' do
197
+ raise RuntimeError, "oops"
198
+ end
199
+ end
200
+
201
+ ensure_world_propagates_error RuntimeError do
202
+ Then 'an outcome' do
203
+ raise RuntimeError, "oops"
204
+ end
205
+ end
206
+ end
207
+
208
+ it 'should inform listeners when it runs a Given, When or Then step' do
209
+ # given
210
+ world = World.create
211
+ mock_listener1 = mock('listener1')
212
+ mock_listener2 = mock('listener2')
213
+ World.add_listener(mock_listener1)
214
+ World.add_listener(mock_listener2)
215
+
216
+ # expect
217
+ mock_listener1.expects(:found_step).with(:given, 'a context')
218
+ mock_listener1.expects(:found_step).with(:when, 'an event')
219
+ mock_listener1.expects(:found_step).with(:then, 'an outcome')
220
+
221
+ mock_listener2.expects(:found_step).with(:given, 'a context')
222
+ mock_listener2.expects(:found_step).with(:when, 'an event')
223
+ mock_listener2.expects(:found_step).with(:then, 'an outcome')
224
+
225
+ # when
226
+ world.instance_eval do
227
+ Given 'a context' do end
228
+ When 'an event' do end
229
+ Then 'an outcome' do end
230
+ end
231
+
232
+ # then
233
+ verify_mocks
234
+ end
235
+
236
+ it 'should tell listeners but not execute the step if it is in dry-run mode' do
237
+ # given
238
+ RBehave::Runner.dry_run = true
239
+ mock_listener = mock('listener')
240
+ World.add_listener(mock_listener)
241
+ $step_invoked = false
242
+ world = World.create
243
+
244
+ # expect
245
+ mock_listener.expects(:found_step).with(:given, 'a context')
246
+
247
+ # when
248
+ world.instance_eval do
249
+ Given 'a context' do
250
+ $step_invoked = true
251
+ end
252
+ end
253
+
254
+ # then
255
+ verify_mocks
256
+ $step_invoked.should be(false)
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,81 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'mocha'
4
+
5
+ # replace rspec's mocks with mocha
6
+ Spec::Runner::configuration.mock_with :mocha
7
+
8
+ # add ensure_that(..), verify(..)
9
+ module Spec::Expectations::ObjectExpectations
10
+ def ensure_that(obj, expr)
11
+ obj.should expr
12
+ end
13
+ end
14
+
15
+ # helper methods
16
+ def pending(message = "todo")
17
+ where = caller[0]
18
+ at_exit { puts "#{where}: #{message}" }
19
+ end
20
+
21
+ def exception_from(&block)
22
+ begin
23
+ yield
24
+ rescue StandardError => e
25
+ e
26
+ end
27
+ end
28
+
29
+ # simplify matchers
30
+
31
+ class Matcher
32
+ def initialize(description, &match_block)
33
+ @description = description
34
+ @match_block = match_block
35
+ end
36
+
37
+ def matches?(actual)
38
+ @actual = actual
39
+ return @match_block.call(actual)
40
+ end
41
+
42
+ def failure_message()
43
+ return %[Expected #@description but got #@actual]
44
+ end
45
+
46
+ def negative_failure_message()
47
+ return %[Expected not to get #@description, but got #@actual]
48
+ end
49
+
50
+ def to_s
51
+ failure_message
52
+ end
53
+ end
54
+
55
+ # custom matchers
56
+
57
+ def contain(string)
58
+ return Matcher.new(%[string containing "#{string}"]) do |actual|
59
+ actual.include? string
60
+ end
61
+ end
62
+
63
+ alias :contains :contain
64
+
65
+ def is(expected)
66
+ return Matcher.new("equal to #{expected}") do |actual| actual == expected end
67
+ end
68
+
69
+ alias :are :is
70
+
71
+ def is_a(type)
72
+ return Matcher.new("object of type #{type}") do |actual|
73
+ actual.is_a? type
74
+ end
75
+ end
76
+
77
+ def matches(pattern)
78
+ return Matcher.new("string matching #{pattern}") do |actual|
79
+ actual =~ pattern
80
+ end
81
+ end
@@ -0,0 +1,29 @@
1
+ module RBehave
2
+ module Documenter
3
+ class PlainTextDocumenter
4
+ def initialize(out)
5
+ @out = out
6
+ end
7
+
8
+ def story_started(title, narrative)
9
+ @out << "Story: #{title}\n#{narrative}\n"
10
+ end
11
+
12
+ def story_ended(title, narrative)
13
+ @out << "\n\n"
14
+ end
15
+
16
+ def scenario_started(story_title, scenario_name)
17
+ @out << "\nScenario: #{scenario_name}\n"
18
+ end
19
+
20
+ def found_step(name, description)
21
+ @out << " #{name.to_s.capitalize} #{description}\n"
22
+ end
23
+
24
+ def method_missing(meth, *args, &block)
25
+ # ignore any other calls
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
1
+ module RBehave
2
+ class PendingException < StandardError
3
+ end
4
+ end
5
+
6
+ class Exception
7
+ class << self
8
+ def backtrace_filters
9
+ @backtrace_filters ||= []
10
+ end
11
+ def add_backtrace_filter(filter)
12
+ backtrace_filters << filter
13
+ end
14
+ end
15
+
16
+ def filtered_backtrace
17
+ trace = backtrace || []
18
+ trace.reject do |line|
19
+ Exception.backtrace_filters.inject(false) do |already_matched, filter|
20
+ already_matched || line =~ filter
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ module RBehave
2
+ module Reporter
3
+ class PlainTextReporter
4
+ def initialize(out)
5
+ @out = out
6
+ @succeeded = 0
7
+ @failed = []
8
+ @pending = []
9
+ end
10
+
11
+ def scenario_succeeded(story_title, scenario_name)
12
+ @out << '.'
13
+ @succeeded += 1
14
+ end
15
+
16
+ def scenario_failed(story_title, scenario_name, err)
17
+ @out << 'F'
18
+ @failed << [story_title, scenario_name, err]
19
+ end
20
+
21
+ def scenario_pending(story_title, scenario_name, msg)
22
+ @pending << [story_title, scenario_name, msg]
23
+ @out << 'P'
24
+ end
25
+
26
+ def run_started(count)
27
+ @count = count
28
+ @out << "Running #@count scenarios:\n"
29
+ end
30
+
31
+ def run_ended
32
+ @out << "\n\n#@count scenarios: #@succeeded succeeded, #{@failed.size} failed, #{@pending.size} pending\n"
33
+ unless @pending.empty?
34
+ @out << "\nPending:\n"
35
+ @pending.each_with_index do |pending, i|
36
+ title, scenario_name, msg = pending
37
+ @out << "#{i+1}) #{title} (#{scenario_name}): #{msg}\n"
38
+ end
39
+ end
40
+ unless @failed.empty?
41
+ @out << "\nFAILURES:"
42
+ @failed.each_with_index do |failure, i|
43
+ title, scenario_name, err = failure
44
+ @out << %[
45
+ #{i+1}) #{title} (#{scenario_name}) FAILED
46
+ #{err.class}: #{err.message}
47
+ #{err.filtered_backtrace.join("\n")}
48
+ ]
49
+ end
50
+ end
51
+ end
52
+
53
+ def method_missing(meth, *args, &block)
54
+ # ignore unexpected callbacks
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,16 @@
1
+ module RBehave
2
+ module Runner
3
+ class ScenarioCollector
4
+ attr_accessor :scenarios
5
+
6
+ def initialize(story)
7
+ @story = story
8
+ @scenarios = []
9
+ end
10
+
11
+ def Scenario(name, &body)
12
+ @scenarios << Scenario.new(@story, name, &body)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,40 @@
1
+ module RBehave
2
+ module Runner
3
+ class ScenarioRunner
4
+ def initialize
5
+ @listeners = []
6
+ end
7
+
8
+ def run(scenario, world)
9
+ begin
10
+ @listeners.each { |l| l.scenario_started(scenario.story.title, scenario.name) }
11
+ run_story_ignoring_scenarios(scenario.story, world)
12
+ world.instance_eval(&scenario.body)
13
+ @listeners.each { |l| l.scenario_succeeded(scenario.story.title, scenario.name) }
14
+ rescue PendingException => e
15
+ @listeners.each { |l| l.scenario_pending(scenario.story.title, scenario.name, e.message) }
16
+ rescue StandardError => e
17
+ @listeners.each { |l| l.scenario_failed(scenario.story.title, scenario.name, e) }
18
+ end
19
+ end
20
+
21
+ def add_listener(listener)
22
+ @listeners << listener
23
+ end
24
+
25
+ private
26
+
27
+ def run_story_ignoring_scenarios(story, world)
28
+ class << world
29
+ def Scenario(name, &block)
30
+ # do nothing
31
+ end
32
+ end
33
+ story.run_in(world)
34
+ class << world
35
+ remove_method(:Scenario)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ module RBehave
2
+ module Runner
3
+ class StoryRunner
4
+ attr_accessor :stories, :scenarios
5
+
6
+ def initialize(scenario_runner, world_creator = World)
7
+ @scenario_runner = scenario_runner
8
+ @world_creator = world_creator
9
+ @stories = []
10
+ @scenarios_by_story = {}
11
+ @scenarios = []
12
+ @listeners = []
13
+ end
14
+
15
+ def Story(title, narrative, &body)
16
+ story = Story.new(title, narrative, &body)
17
+ @stories << story
18
+
19
+ # collect scenarios
20
+ collector = ScenarioCollector.new(story)
21
+ story.run_in(collector)
22
+ @scenarios += collector.scenarios
23
+ @scenarios_by_story[story.title] = collector.scenarios
24
+ end
25
+
26
+ def run_stories
27
+ @listeners.each { |l| l.run_started(scenarios.size) }
28
+ @stories.each do |story|
29
+ @listeners.each { |l| l.story_started(story.title, story.narrative) }
30
+ scenarios = @scenarios_by_story[story.title]
31
+ scenarios.each do |scenario|
32
+ @scenario_runner.run(scenario, @world_creator.create)
33
+ end
34
+ @listeners.each { |l| l.story_ended(story.title, story.narrative) }
35
+ end
36
+ @listeners.each { |l| l.run_ended }
37
+ end
38
+
39
+ def add_listener(listener)
40
+ @listeners << listener
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,11 @@
1
+ module RBehave
2
+ class Scenario
3
+ attr_accessor :name, :body, :story
4
+
5
+ def initialize(story, name, &body)
6
+ @story = story
7
+ @name = name
8
+ @body = body
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module RBehave
2
+ class Story
3
+ attr_reader :title, :narrative
4
+
5
+ def initialize(title, narrative, &body)
6
+ @body = body
7
+ @title = title
8
+ @narrative = narrative
9
+ end
10
+
11
+ def run_in(container)
12
+ container.instance_eval(&@body)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module Rbehave #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
@@ -0,0 +1,55 @@
1
+ module RBehave
2
+ =begin
3
+ A World represents the actual instance a scenario will run in.
4
+
5
+ RBehave ensures any instance variables and methods defined anywhere
6
+ in a story block are available to all the scenarios. This includes
7
+ variables that are created or referenced inside Given, When and Then
8
+ blocks.
9
+ =end
10
+ module World
11
+ # store steps and listeners in the singleton metaclass.
12
+ # This serves both to keep them out of the way and to
13
+ # make them available to all instances.
14
+ class << self
15
+ def create(cls = Object)
16
+ cls.new.extend(World)
17
+ end
18
+
19
+ def store_and_call(instance, type, name, &block)
20
+ @step_mother ||= Hash.new {|hsh,key| hsh[key] = Hash.new }
21
+ if block_given?
22
+ @step_mother[type][name] = block
23
+ else
24
+ block = @step_mother[type][name]
25
+ end
26
+ listeners.each { |l| l.found_step(type, name) }
27
+ instance.instance_eval(&block) unless RBehave::Runner.dry_run?
28
+ end
29
+
30
+ def listeners
31
+ @listeners ||= []
32
+ end
33
+
34
+ def add_listener(listener)
35
+ listeners << listener
36
+ end
37
+ end
38
+
39
+ def Given(name, &block)
40
+ World.store_and_call self, :given, name, &block
41
+ end
42
+
43
+ def When(name, &block)
44
+ World.store_and_call self, :when, name, &block
45
+ end
46
+
47
+ def Then(name, &block)
48
+ World.store_and_call self, :then, name, &block
49
+ end
50
+
51
+ def pending(message = 'todo')
52
+ raise PendingException, message
53
+ end
54
+ end
55
+ end