kookaburra 0.22.3 → 0.23.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +18 -11
- data/VERSION +1 -1
- data/kookaburra.gemspec +5 -2
- data/lib/kookaburra.rb +9 -6
- data/lib/kookaburra/configuration.rb +11 -0
- data/lib/kookaburra/mental_model.rb +10 -18
- data/lib/kookaburra/mental_model_matcher.rb +130 -0
- data/lib/kookaburra/test_helpers.rb +22 -0
- data/spec/integration/test_a_rack_application_spec.rb +15 -4
- data/spec/kookaburra/configuration_spec.rb +20 -0
- data/spec/kookaburra/mental_model_matcher_spec.rb +234 -0
- data/spec/kookaburra/mental_model_spec.rb +33 -2
- data/spec/kookaburra/test_helpers_spec.rb +81 -0
- data/spec/kookaburra_spec.rb +6 -12
- metadata +8 -5
data/README.markdown
CHANGED
@@ -165,29 +165,34 @@ your own UIComponents. This method exists to verify preconditions and provide
|
|
165
165
|
more informative error messages; it is not intended to be used for test
|
166
166
|
verifications.
|
167
167
|
|
168
|
+
`Kookaburra::TestHelpers` provides a convenient way to make assertions about the
|
169
|
+
mental model. If you are using Test::Unit, see
|
170
|
+
`Kookaburra::TestHelpers#assert_mental_model_of`; for RSpec, see
|
171
|
+
`Kookaburra::TestHelpers#match_mental_model_of`.
|
172
|
+
|
168
173
|
Given the Cucumber scenario above, here is how the test implementation layer
|
169
174
|
might look:
|
170
175
|
|
171
176
|
# step_definitions/various_steps.rb
|
172
177
|
|
173
178
|
Given "I have an existing account" do
|
174
|
-
given.existing_account
|
179
|
+
given.existing_account
|
175
180
|
end
|
176
181
|
|
177
182
|
Given "I have previously specified default payment options" do
|
178
|
-
given.
|
183
|
+
given.default_payment_options_specified
|
179
184
|
end
|
180
185
|
|
181
186
|
Given "I have previously specified default shipping options" do
|
182
|
-
given.
|
187
|
+
given.default_shipping_options_specified
|
183
188
|
end
|
184
189
|
|
185
190
|
Given "I have an item in my shopping cart" do
|
186
|
-
given.an_item_in_my_shopping_cart
|
191
|
+
given.an_item_in_my_shopping_cart
|
187
192
|
end
|
188
193
|
|
189
194
|
When "I sign in to my account" do
|
190
|
-
ui.sign_in
|
195
|
+
ui.sign_in
|
191
196
|
end
|
192
197
|
|
193
198
|
When "I choose to check out" do
|
@@ -199,11 +204,13 @@ might look:
|
|
199
204
|
end
|
200
205
|
|
201
206
|
Then "I see that my default payment options will be used" do
|
202
|
-
ui.order_summary.payment_options.should
|
207
|
+
ui.order_summary.payment_options.should match_mental_model_of(:default_payment_options)
|
208
|
+
# Or if you prefer Test::Unit style assertions...
|
209
|
+
# assert_mental_model_matches(:default_payment_options, ui.order_summary.payment_options)
|
203
210
|
end
|
204
211
|
|
205
212
|
Then "I see that my default shipping options will be used" do
|
206
|
-
ui.order_summary.shipping_options.should
|
213
|
+
ui.order_summary.shipping_options.should match_mental_model_of(:default_shipping_options)
|
207
214
|
end
|
208
215
|
|
209
216
|
The step definitions contain neither explicitly shared state (instance
|
@@ -281,17 +288,17 @@ share a `MentalModel` instance, which is available to both of them via their
|
|
281
288
|
|
282
289
|
The `MentalModel` instance will return a `MentalModel::Collection` for any method
|
283
290
|
called on the object. The `MentalModel::Collection` object behaves like a `Hash`
|
284
|
-
for the most part
|
291
|
+
for the most part; however, it will raise a `Kookaburra::UnknownKeyError` if you
|
285
292
|
try to access a key that has not yet been assigned a value.
|
286
293
|
|
287
294
|
Deletions (via `#delete` or `#delete_if`) will actually remove the key/value
|
288
|
-
pair from the collection, but add it to a
|
295
|
+
pair from the collection, but add it to a sub-collection (available at
|
289
296
|
`MentalModel::Collection#deleted`). This reflects the fact that the user's
|
290
297
|
mental model of the dataset would also include any intentional exceptions -
|
291
|
-
the user will, for example,
|
298
|
+
the user will, for example, want to verify that an item they deleted does
|
292
299
|
not appear to be available in the system.
|
293
300
|
|
294
|
-
Here's
|
301
|
+
Here's an example of MentalModel behavior:
|
295
302
|
|
296
303
|
mental_model = MentalModel.new
|
297
304
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.23.0
|
data/kookaburra.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "kookaburra"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.23.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["John Wilger", "Sam Livingston-Gray", "Ravi Gadad"]
|
12
|
-
s.date = "2012-
|
12
|
+
s.date = "2012-04-02"
|
13
13
|
s.description = "Cucumber + Capybara = Kookaburra? It made sense at the time."
|
14
14
|
s.email = "johnwilger@gmail.com"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -37,6 +37,7 @@ Gem::Specification.new do |s|
|
|
37
37
|
"lib/kookaburra/given_driver.rb",
|
38
38
|
"lib/kookaburra/json_api_driver.rb",
|
39
39
|
"lib/kookaburra/mental_model.rb",
|
40
|
+
"lib/kookaburra/mental_model_matcher.rb",
|
40
41
|
"lib/kookaburra/test_helpers.rb",
|
41
42
|
"lib/kookaburra/ui_driver.rb",
|
42
43
|
"lib/kookaburra/ui_driver/ui_component.rb",
|
@@ -45,7 +46,9 @@ Gem::Specification.new do |s|
|
|
45
46
|
"spec/kookaburra/api_driver_spec.rb",
|
46
47
|
"spec/kookaburra/configuration_spec.rb",
|
47
48
|
"spec/kookaburra/json_api_driver_spec.rb",
|
49
|
+
"spec/kookaburra/mental_model_matcher_spec.rb",
|
48
50
|
"spec/kookaburra/mental_model_spec.rb",
|
51
|
+
"spec/kookaburra/test_helpers_spec.rb",
|
49
52
|
"spec/kookaburra/ui_driver/ui_component/address_bar_spec.rb",
|
50
53
|
"spec/kookaburra/ui_driver/ui_component_spec.rb",
|
51
54
|
"spec/kookaburra/ui_driver_spec.rb",
|
data/lib/kookaburra.rb
CHANGED
@@ -62,10 +62,7 @@ class Kookaburra
|
|
62
62
|
@ui ||= @ui_driver_class.new(@configuration)
|
63
63
|
end
|
64
64
|
|
65
|
-
# Returns a
|
66
|
-
# However, this is neither a deep copy nor a deep freeze, so it is possible
|
67
|
-
# that you could modify data outside of your GivenDriver or UIDriver. Just
|
68
|
-
# don't do that. Trust me.
|
65
|
+
# Returns a deep-dup of the specified {MentalModel::Collection}.
|
69
66
|
#
|
70
67
|
# This access is provided so that you can reference the current mental model
|
71
68
|
# within your test implementation and make assertions about the state
|
@@ -74,10 +71,16 @@ class Kookaburra
|
|
74
71
|
# @example
|
75
72
|
# given.a_widget(:foo)
|
76
73
|
# ui.create_a_new_widget(:bar)
|
77
|
-
# ui.widget_list.widgets.should == k.get_data(:widgets).
|
74
|
+
# ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:foo, :bar)
|
78
75
|
#
|
79
76
|
# @return [Kookaburra::MentalModel::Collection]
|
80
77
|
def get_data(collection_name)
|
81
|
-
@configuration.mental_model.send(collection_name).dup
|
78
|
+
@configuration.mental_model.send(collection_name).dup
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def __mental_model__
|
84
|
+
@configuration.mental_model
|
82
85
|
end
|
83
86
|
end
|
@@ -66,5 +66,16 @@ class Kookaburra
|
|
66
66
|
@server_error_detection
|
67
67
|
end
|
68
68
|
end
|
69
|
+
|
70
|
+
# The parsed version of the {#app_host}
|
71
|
+
#
|
72
|
+
# This is useful if, for example, you are testing a multi-host application
|
73
|
+
# and need to change the hostname that will be accessed but want to keep the
|
74
|
+
# originally-specified port
|
75
|
+
#
|
76
|
+
# @return [URI] A URI object created from the {#app_host} string
|
77
|
+
def app_host_uri
|
78
|
+
URI.parse(app_host)
|
79
|
+
end
|
69
80
|
end
|
70
81
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'delegate'
|
2
2
|
require 'kookaburra/exceptions'
|
3
|
+
require 'active_support/core_ext/hash'
|
3
4
|
|
4
5
|
class Kookaburra
|
5
6
|
# Each instance of {Kookaburra} has its own instance of MentalModel. This object
|
@@ -50,11 +51,13 @@ class Kookaburra
|
|
50
51
|
class Collection < SimpleDelegator
|
51
52
|
# @param [String] name The name of the collection. Used to provide
|
52
53
|
# helpful error messages when unknown keys are accessed.
|
53
|
-
|
54
|
+
# @param [Hash] init_data Preloads specific data into the collection
|
55
|
+
def initialize(name, init_data = nil)
|
54
56
|
@name = name
|
55
57
|
data = Hash.new do |hash, key|
|
56
58
|
raise UnknownKeyError, "Can't find mental_model.#{@name}[#{key.inspect}]. Did you forget to set it?"
|
57
59
|
end
|
60
|
+
data.merge!(init_data) unless init_data.nil?
|
58
61
|
super(data)
|
59
62
|
end
|
60
63
|
|
@@ -66,23 +69,6 @@ class Kookaburra
|
|
66
69
|
self.object_id == other.object_id
|
67
70
|
end
|
68
71
|
|
69
|
-
# Returns the values of multiple keys from the collection.
|
70
|
-
#
|
71
|
-
# Unlike the `Hash#slice` implementation provided by ActiveSupport, this
|
72
|
-
# method returns an array of the values rather than a Hash.
|
73
|
-
#
|
74
|
-
# @param keys a list of keys to fetch from the collection.
|
75
|
-
#
|
76
|
-
# @return [Array] the values matching the specified index keys
|
77
|
-
#
|
78
|
-
# @raise [Kookaburra::UnknownKeyError] if any of the specified keys have
|
79
|
-
# not been set
|
80
|
-
def slice(*keys)
|
81
|
-
results = keys.map do |key|
|
82
|
-
self[key]
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
72
|
# Deletes a key/value pair from the collection, and persists the deleted pair
|
87
73
|
# in a subcollection.
|
88
74
|
#
|
@@ -121,6 +107,12 @@ class Kookaburra
|
|
121
107
|
move = lambda { |k,v| deleted[k] = v; true }
|
122
108
|
super { |k,v| block.call(k,v) && move.call(k,v) }
|
123
109
|
end
|
110
|
+
|
111
|
+
def dup
|
112
|
+
new_data = {}.merge(self)
|
113
|
+
new_data = Marshal.load(Marshal.dump(new_data))
|
114
|
+
self.class.new(@name, new_data)
|
115
|
+
end
|
124
116
|
end
|
125
117
|
end
|
126
118
|
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'kookaburra/mental_model'
|
2
|
+
|
3
|
+
class Kookaburra
|
4
|
+
class MentalModel
|
5
|
+
# This is a custom matcher that matches the RSpec matcher API.
|
6
|
+
#
|
7
|
+
# @see Kookaburra::TestHelpers#match_mental_model_of
|
8
|
+
# @see Kookaburra::TestHelpers#assert_mental_model_of
|
9
|
+
class Matcher
|
10
|
+
def initialize(mental_model, collection_key)
|
11
|
+
@collection_key = collection_key
|
12
|
+
|
13
|
+
mental_model.send(collection_key).tap do |collection|
|
14
|
+
@expected = collection
|
15
|
+
@unexpected = collection.deleted
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Specifies that result should be limited to the given keys from the
|
20
|
+
# mental model.
|
21
|
+
#
|
22
|
+
# Useful if you are looking at a filtered result. That is, your mental
|
23
|
+
# model contains elements { A, B, C }, but you only expect to see element
|
24
|
+
# A.
|
25
|
+
#
|
26
|
+
# @param [Array] collection_keys The keys used in your mental model to
|
27
|
+
# reference the data
|
28
|
+
# @return [self]
|
29
|
+
def only(*collection_keys)
|
30
|
+
keepers = @expected.slice(*collection_keys)
|
31
|
+
tossers = @expected.except(*collection_keys)
|
32
|
+
|
33
|
+
@expected = keepers
|
34
|
+
@unexpected.merge! tossers
|
35
|
+
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
# Reads better than {#only} with no args
|
40
|
+
#
|
41
|
+
# @return [self]
|
42
|
+
def expecting_nothing
|
43
|
+
only
|
44
|
+
end
|
45
|
+
|
46
|
+
# The result contains everything that was expected to be found and nothing
|
47
|
+
# that was unexpected.
|
48
|
+
#
|
49
|
+
# (Part of the RSpec protocol for custom matchers.)
|
50
|
+
#
|
51
|
+
# @param [Array] actual This is the data observed that you are attempting
|
52
|
+
# to match against the mental model.
|
53
|
+
# @return Boolean
|
54
|
+
def matches?(actual)
|
55
|
+
@actual = actual
|
56
|
+
expected_items_not_found.empty? && unexpected_items_found.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Message to be printed when observed reality does not conform to
|
60
|
+
# mental model.
|
61
|
+
#
|
62
|
+
# (Part of the RSpec protocol for custom matchers.)
|
63
|
+
#
|
64
|
+
# @return String
|
65
|
+
def failure_message_for_should
|
66
|
+
message = "expected #{@collection_key} to match the user's mental model, but:\n"
|
67
|
+
if expected_items_not_found.present?
|
68
|
+
message += "expected to be present: #{pp_array(expected_items)}\n"
|
69
|
+
message += "the missing elements were: #{pp_array(expected_items_not_found)}\n"
|
70
|
+
end
|
71
|
+
if unexpected_items_found.present?
|
72
|
+
message += "expected to not be present: #{pp_array(unexpected_items)}\n"
|
73
|
+
message += "the unexpected extra elements: #{pp_array(unexpected_items_found)}\n"
|
74
|
+
end
|
75
|
+
message
|
76
|
+
end
|
77
|
+
|
78
|
+
# Message to be printed when observed reality does conform to mental
|
79
|
+
# model, but you did not expect it to. (To be honest, we can't think of
|
80
|
+
# why you would want this, but it is included for the sake of RSpec
|
81
|
+
# compatibility.)
|
82
|
+
#
|
83
|
+
# (Part of the RSpec protocol for custom matchers.)
|
84
|
+
#
|
85
|
+
# @return String
|
86
|
+
def failure_message_for_should_not
|
87
|
+
"expected #{@collection_key} not to match the user's mental model"
|
88
|
+
end
|
89
|
+
|
90
|
+
# (Part of the RSpec protocol for custom matchers.)
|
91
|
+
#
|
92
|
+
# @return String
|
93
|
+
def description
|
94
|
+
"match the user's mental model of #{@collection_key}"
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def expected_items; @expected.values; end
|
100
|
+
def unexpected_items; @unexpected.values; end
|
101
|
+
|
102
|
+
def expected_items_not_found
|
103
|
+
difference_between_arrays(expected_items, @actual)
|
104
|
+
end
|
105
|
+
|
106
|
+
def unexpected_items_found
|
107
|
+
unexpected_items_not_found = difference_between_arrays(unexpected_items, @actual)
|
108
|
+
difference_between_arrays(unexpected_items, unexpected_items_not_found)
|
109
|
+
end
|
110
|
+
|
111
|
+
# (Swiped from RSpec's array matcher)
|
112
|
+
# Returns the difference of arrays, accounting for duplicates.
|
113
|
+
# e.g., difference_between_arrays([1, 2, 3, 3], [1, 2, 3]) # => [3]
|
114
|
+
def difference_between_arrays(array_1, array_2)
|
115
|
+
difference = array_1.dup
|
116
|
+
array_2.each do |element|
|
117
|
+
if index = difference.index(element)
|
118
|
+
difference.delete_at(index)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
difference
|
122
|
+
end
|
123
|
+
|
124
|
+
def pp_array(array)
|
125
|
+
array = array.sort if array.all? { |e| e.respond_to?(:<=>) }
|
126
|
+
array.inspect
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'kookaburra'
|
2
|
+
require 'kookaburra/mental_model_matcher'
|
2
3
|
require 'active_support/core_ext/module/delegation'
|
3
4
|
|
4
5
|
class Kookaburra
|
@@ -96,5 +97,26 @@ class Kookaburra
|
|
96
97
|
# @method ui
|
97
98
|
# Delegates to {#k}
|
98
99
|
delegate :ui, :to => :k
|
100
|
+
|
101
|
+
# RSpec-style custom matcher that compares a given array with
|
102
|
+
# the current state of one named collection in the mental model
|
103
|
+
#
|
104
|
+
# @see Kookaburra::MentalModel::Matcher
|
105
|
+
def match_mental_model_of(collection_key)
|
106
|
+
MentalModel::Matcher.new(k.send(:__mental_model__), collection_key)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Custom assertion for Test::Unit-style tests
|
110
|
+
# (really, anything that uses #assert(predicate, message = nil))
|
111
|
+
#
|
112
|
+
# @see Kookaburra::MentalModel::Matcher
|
113
|
+
def assert_mental_model_matches(collection_key, actual, message = nil)
|
114
|
+
matcher = match_mental_model_of(collection_key)
|
115
|
+
result = matcher.matches?(actual)
|
116
|
+
return if !!result # don't even bother
|
117
|
+
|
118
|
+
message ||= matcher.failure_message_for_should
|
119
|
+
assert result, message
|
120
|
+
end
|
99
121
|
end
|
100
122
|
end
|
@@ -294,7 +294,7 @@ describe "testing a Rack application with Kookaburra" do
|
|
294
294
|
|
295
295
|
def delete_widget(name)
|
296
296
|
assert widget_list.visible?, "Widget list is not visible!"
|
297
|
-
widget_list.choose_to_delete_widget(mental_model.widgets
|
297
|
+
widget_list.choose_to_delete_widget(mental_model.widgets.delete(name))
|
298
298
|
end
|
299
299
|
end
|
300
300
|
|
@@ -336,13 +336,24 @@ describe "testing a Rack application with Kookaburra" do
|
|
336
336
|
|
337
337
|
ui.sign_in(:bob)
|
338
338
|
ui.view_widget_list
|
339
|
-
|
339
|
+
|
340
|
+
# The following two lines are two different ways to shave the yak, but
|
341
|
+
# the second one does more to match against the full state of the mental
|
342
|
+
# model, provides better failure messages, and is shorter.
|
343
|
+
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_b)
|
344
|
+
ui.widget_list.widgets.should match_mental_model_of(:widgets)
|
340
345
|
|
341
346
|
ui.create_new_widget(:widget_c, :name => 'Bar')
|
342
|
-
|
347
|
+
|
348
|
+
# As above, these are equivalent, but the second line is preferred.
|
349
|
+
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_b, :widget_c)
|
350
|
+
ui.widget_list.widgets.should match_mental_model_of(:widgets)
|
343
351
|
|
344
352
|
ui.delete_widget(:widget_b)
|
345
|
-
|
353
|
+
|
354
|
+
# As above, these are equivalent, but the second line is preferred.
|
355
|
+
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_c)
|
356
|
+
ui.widget_list.widgets.should match_mental_model_of(:widgets)
|
346
357
|
end
|
347
358
|
end
|
348
359
|
end
|
@@ -15,4 +15,24 @@ describe Kookaburra::Configuration do
|
|
15
15
|
subject.server_error_detection.should == block
|
16
16
|
end
|
17
17
|
end
|
18
|
+
|
19
|
+
describe '#app_host_uri' do
|
20
|
+
it 'returns a URI version of the #app_host attribute via URI.parse' do
|
21
|
+
URI.should_receive(:parse) \
|
22
|
+
.with('http://example.com') \
|
23
|
+
.and_return(:a_parsed_uri)
|
24
|
+
subject.app_host = 'http://example.com'
|
25
|
+
subject.app_host_uri.should == :a_parsed_uri
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'changes if #app_host changes' do
|
29
|
+
URI.stub!(:parse) do |url|
|
30
|
+
url.to_sym
|
31
|
+
end
|
32
|
+
subject.app_host = 'http://example.com'
|
33
|
+
subject.app_host_uri.should == 'http://example.com'.to_sym
|
34
|
+
subject.app_host = 'http://foo.example.com'
|
35
|
+
subject.app_host_uri.should == 'http://foo.example.com'.to_sym
|
36
|
+
end
|
37
|
+
end
|
18
38
|
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
require 'kookaburra/mental_model_matcher'
|
2
|
+
|
3
|
+
# Makes the specs themselves a bit less verbose. You should probably read the
|
4
|
+
# specs first, though.
|
5
|
+
module MentalModelMatcherMacros
|
6
|
+
def self.included(receiver)
|
7
|
+
receiver.extend ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def pp_array(array)
|
12
|
+
array = array.sort if array.all? { |e| e.respond_to?(:<=>) }
|
13
|
+
array.inspect
|
14
|
+
end
|
15
|
+
|
16
|
+
def it_matches
|
17
|
+
it "matches" do
|
18
|
+
matcher.matches?(target).should be_true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def it_doesnt_match
|
23
|
+
it "doesn't match" do
|
24
|
+
matcher.matches?(target).should be_false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def from_line
|
29
|
+
'(line %d)' % caller(2).first.split(':').last
|
30
|
+
end
|
31
|
+
|
32
|
+
def it_complains_about_missing(missing, options)
|
33
|
+
expected = options[:expected]
|
34
|
+
it "complains about a mismatch #{from_line}" do
|
35
|
+
failure_msg.should include("expected widgets to match the user's mental model, but:")
|
36
|
+
end
|
37
|
+
it "says what it expected to be present #{from_line}" do
|
38
|
+
failure_msg.should include("expected to be present: #{pp_array(expected)}")
|
39
|
+
end
|
40
|
+
it "complains about missing items #{from_line}" do
|
41
|
+
failure_msg.should include("the missing elements were: #{pp_array(missing)}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def it_complains_about_extra(extra, options)
|
46
|
+
unexpected = options[:unexpected]
|
47
|
+
it "complains about a mismatch #{from_line}" do
|
48
|
+
failure_msg.should include("expected widgets to match the user's mental model, but:")
|
49
|
+
end
|
50
|
+
it "says what it expected not to find #{from_line}" do
|
51
|
+
failure_msg.should include("expected to not be present: #{pp_array(unexpected)}")
|
52
|
+
end
|
53
|
+
it "complains about extra items #{from_line}" do
|
54
|
+
failure_msg.should include("the unexpected extra elements: #{pp_array(extra)}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def matcher_for(collection_key)
|
60
|
+
Kookaburra::MentalModel::Matcher.new(mm, collection_key).tap do |m|
|
61
|
+
m.matches?(target)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def pp_array(array)
|
66
|
+
self.class.pp_array(array)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe Kookaburra::MentalModel::Matcher do
|
71
|
+
include MentalModelMatcherMacros
|
72
|
+
|
73
|
+
let(:mm) { Kookaburra::MentalModel.new }
|
74
|
+
let(:matcher) { matcher_for(:widgets) }
|
75
|
+
let(:failure_msg) { matcher.failure_message_for_should }
|
76
|
+
|
77
|
+
def self.foo; 'FOO' ; end
|
78
|
+
def self.bar; 'BAR' ; end
|
79
|
+
def self.yak; 'YAK' ; end
|
80
|
+
let(:foo) { self.class.foo }
|
81
|
+
let(:bar) { self.class.bar }
|
82
|
+
let(:yak) { self.class.yak }
|
83
|
+
|
84
|
+
context "when mental model is [foo];" do
|
85
|
+
before(:each) do
|
86
|
+
mm.widgets[:foo] = foo
|
87
|
+
end
|
88
|
+
|
89
|
+
context "for [] (foo missing)" do
|
90
|
+
let(:target) { [] }
|
91
|
+
it_doesnt_match
|
92
|
+
it_complains_about_missing [foo], :expected => [foo]
|
93
|
+
end
|
94
|
+
|
95
|
+
context "for [foo] (OK: exact match)" do
|
96
|
+
let(:target) { [foo] }
|
97
|
+
it_matches
|
98
|
+
end
|
99
|
+
|
100
|
+
context "for [foo, bar] (OK: bar ignored)" do
|
101
|
+
let(:target) { [foo, bar] }
|
102
|
+
it_matches
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "when mental model is [];" do
|
107
|
+
context "for [] (OK)" do
|
108
|
+
let(:target) { [] }
|
109
|
+
it_matches
|
110
|
+
end
|
111
|
+
|
112
|
+
context "for [foo] (OK: foo ignored)" do
|
113
|
+
let(:target) { [foo] }
|
114
|
+
it_matches
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context "when mental model is [foo, bar];" do
|
119
|
+
before(:each) do
|
120
|
+
mm.widgets[:foo] = foo
|
121
|
+
mm.widgets[:bar] = bar
|
122
|
+
end
|
123
|
+
|
124
|
+
context "for [] (foo, bar missing)" do
|
125
|
+
let(:target) { [] }
|
126
|
+
it_doesnt_match
|
127
|
+
it_complains_about_missing [foo, bar], :expected => [foo, bar]
|
128
|
+
end
|
129
|
+
|
130
|
+
context "for [foo] (bar missing)" do
|
131
|
+
let(:target) { [foo] }
|
132
|
+
it_doesnt_match
|
133
|
+
it_complains_about_missing [bar], :expected => [foo, bar]
|
134
|
+
end
|
135
|
+
|
136
|
+
context "for [foo, bar] (OK: exact match)" do
|
137
|
+
let(:target) { [foo, bar] }
|
138
|
+
it_matches
|
139
|
+
end
|
140
|
+
|
141
|
+
context "for [foo, bar, yak] (OK: foo, bar expected; yak ignored)" do
|
142
|
+
let(:target) { [foo, bar, yak] }
|
143
|
+
it_matches
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "when mental model is [foo], not expecting [bar];" do
|
148
|
+
before(:each) do
|
149
|
+
mm.widgets[:foo] = foo
|
150
|
+
mm.widgets[:bar] = bar
|
151
|
+
mm.widgets.delete(:bar)
|
152
|
+
end
|
153
|
+
|
154
|
+
context "for [] (foo missing)" do
|
155
|
+
let(:target) { [] }
|
156
|
+
it_doesnt_match
|
157
|
+
it_complains_about_missing [foo], :expected => [foo]
|
158
|
+
end
|
159
|
+
|
160
|
+
context "for [bar] (foo missing, bar deleted)" do
|
161
|
+
let(:target) { [bar] }
|
162
|
+
it_doesnt_match
|
163
|
+
it_complains_about_missing [foo], :expected => [foo]
|
164
|
+
it_complains_about_extra [bar], :unexpected => [bar]
|
165
|
+
end
|
166
|
+
|
167
|
+
context "for [foo, bar] (bar deleted)" do
|
168
|
+
let(:target) { [foo, bar] }
|
169
|
+
it_doesnt_match
|
170
|
+
it_complains_about_extra [bar], :unexpected => [bar]
|
171
|
+
end
|
172
|
+
|
173
|
+
context "for [foo] (OK: foo expected, bar not found)" do
|
174
|
+
let(:target) { [foo] }
|
175
|
+
it_matches
|
176
|
+
end
|
177
|
+
|
178
|
+
context "for [foo, yak] (OK: foo expected; yak ignored)" do
|
179
|
+
let(:target) { [foo, yak] }
|
180
|
+
it_matches
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe "postfix scoping methods" do
|
185
|
+
context "when mental model is [foo, bar];" do
|
186
|
+
before(:each) do
|
187
|
+
mm.widgets[:foo] = foo
|
188
|
+
mm.widgets[:bar] = bar
|
189
|
+
end
|
190
|
+
|
191
|
+
context "but scoped to .only(:foo)" do
|
192
|
+
let(:matcher) { matcher_for(:widgets).only(:foo) }
|
193
|
+
|
194
|
+
context "for [foo] (OK)" do
|
195
|
+
let(:target) { [foo] }
|
196
|
+
it_matches
|
197
|
+
end
|
198
|
+
|
199
|
+
context "for [foo, bar] (not expecting [bar])" do
|
200
|
+
let(:target) { [foo, bar] }
|
201
|
+
it_doesnt_match
|
202
|
+
it_complains_about_extra [bar], :unexpected => [bar]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
context "when mental model is [foo];" do
|
208
|
+
before(:each) do
|
209
|
+
mm.widgets[:foo] = foo
|
210
|
+
end
|
211
|
+
|
212
|
+
context "but scoped with .expecting_nothing" do
|
213
|
+
let(:matcher) { matcher_for(:widgets).expecting_nothing }
|
214
|
+
|
215
|
+
context "for [] (OK)" do
|
216
|
+
let(:target) { [] }
|
217
|
+
it_matches
|
218
|
+
end
|
219
|
+
|
220
|
+
context "for [foo] (unexpected foo)" do
|
221
|
+
let(:target) { [foo] }
|
222
|
+
it_doesnt_match
|
223
|
+
it_complains_about_extra [foo], :unexpected => [foo]
|
224
|
+
end
|
225
|
+
|
226
|
+
context "for [foo, bar] (unexpected [foo]; bar ignored)" do
|
227
|
+
let(:target) { [foo, bar] }
|
228
|
+
it_doesnt_match
|
229
|
+
it_complains_about_extra [foo], :unexpected => [foo]
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -14,12 +14,21 @@ describe Kookaburra::MentalModel do
|
|
14
14
|
describe Kookaburra::MentalModel::Collection do
|
15
15
|
let(:collection) { Kookaburra::MentalModel::Collection.new('widgets') }
|
16
16
|
|
17
|
-
describe '#
|
17
|
+
describe '#values_at' do
|
18
18
|
it 'returns an array of items matching the specified keys' do
|
19
19
|
collection[:foo] = 'foo'
|
20
20
|
collection[:bar] = 'bar'
|
21
21
|
collection[:baz] = 'baz'
|
22
|
-
collection.
|
22
|
+
collection.values_at(:foo, :baz).should == %w(foo baz)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#slice' do
|
27
|
+
it 'returns a hash of items matching the specified keys' do
|
28
|
+
collection[:foo] = 'foo'
|
29
|
+
collection[:bar] = 'bar'
|
30
|
+
collection[:baz] = 'baz'
|
31
|
+
collection.slice(:foo, :baz).should == { :foo => 'foo', :baz => 'baz' }
|
23
32
|
end
|
24
33
|
end
|
25
34
|
|
@@ -81,5 +90,27 @@ describe Kookaburra::MentalModel do
|
|
81
90
|
lambda { collection[:foo] }.should \
|
82
91
|
raise_error(Kookaburra::UnknownKeyError, "Can't find mental_model.widgets[:foo]. Did you forget to set it?")
|
83
92
|
end
|
93
|
+
|
94
|
+
describe '#dup' do
|
95
|
+
it 'returns a different object' do
|
96
|
+
new_collection = collection.dup
|
97
|
+
new_collection.__id__.should_not === collection.__id__
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'returns an object with equal values to the original' do
|
101
|
+
collection[:foo] = :bar
|
102
|
+
collection[:baz] = :bam
|
103
|
+
new_collection = collection.dup
|
104
|
+
new_collection[:foo].should == :bar
|
105
|
+
new_collection[:baz].should == :bam
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'is a deep copy' do
|
109
|
+
collection[:foo] = {:bar => 'baz'}
|
110
|
+
new_collection = collection.dup
|
111
|
+
new_collection[:foo][:bar].should == 'baz'
|
112
|
+
new_collection[:foo][:bar].__id__.should_not === collection[:foo][:bar].__id__
|
113
|
+
end
|
114
|
+
end
|
84
115
|
end
|
85
116
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'kookaburra/test_helpers'
|
2
|
+
|
3
|
+
describe Kookaburra::TestHelpers do
|
4
|
+
include Kookaburra::TestHelpers
|
5
|
+
|
6
|
+
before(:all) do
|
7
|
+
Kookaburra.configure do |c|
|
8
|
+
c.given_driver_class = Kookaburra::GivenDriver
|
9
|
+
c.ui_driver_class = Kookaburra::UIDriver
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:all) do
|
14
|
+
Kookaburra.configure do |c|
|
15
|
+
c.given_driver_class = nil
|
16
|
+
c.ui_driver_class = nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#k" do
|
21
|
+
it "returns an instance of Kookaburra" do
|
22
|
+
k.should be_kind_of(Kookaburra)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "memoizes" do
|
26
|
+
a = k; b = k
|
27
|
+
a.should equal(b)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "methods delegated to #k" do
|
32
|
+
it "includes #given" do
|
33
|
+
k.should_receive(:given)
|
34
|
+
given
|
35
|
+
end
|
36
|
+
|
37
|
+
it "includes #ui" do
|
38
|
+
k.should_receive(:ui)
|
39
|
+
ui
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "methods related to the mental model" do
|
44
|
+
before(:each) do
|
45
|
+
mm = k.send(:__mental_model__)
|
46
|
+
mm.widgets[:foo] = 'FOO'
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#match_mental_model_of" do
|
50
|
+
it "does a positive match" do
|
51
|
+
['FOO'].should match_mental_model_of(:widgets)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "does a negative match" do
|
55
|
+
['BAR'].should_not match_mental_model_of(:widgets)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#assert_mental_model_matches" do
|
60
|
+
it "does a positive assertion" do
|
61
|
+
actual = ['FOO']
|
62
|
+
actual.should match_mental_model_of(:widgets) # Sanity check
|
63
|
+
self.should_receive(:assert).never
|
64
|
+
self.assert_mental_model_matches(:widgets, actual)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "does a negative assertion" do
|
68
|
+
actual = ['BAR']
|
69
|
+
self.should_receive(:assert).with(false, kind_of(String))
|
70
|
+
self.assert_mental_model_matches(:widgets, actual)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "does a negative assertion with a custom message" do
|
74
|
+
actual = ['YAK']
|
75
|
+
psa = 'Put the razor down and step away!'
|
76
|
+
self.should_receive(:assert).with(false, psa)
|
77
|
+
self.assert_mental_model_matches(:widgets, actual, psa)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/spec/kookaburra_spec.rb
CHANGED
@@ -30,18 +30,12 @@ describe Kookaburra do
|
|
30
30
|
end
|
31
31
|
|
32
32
|
describe '#get_data' do
|
33
|
-
it 'returns a
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
it 'does not return the same object that is the test data collection' do
|
40
|
-
k.get_data(:foos).should_not === k.get_data(:foos)
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'returns a frozen object' do
|
44
|
-
k.get_data(:foos).should be_frozen
|
33
|
+
it 'returns a dup of the specified MentalModel::Collection' do
|
34
|
+
collection = stub('MentalModel::Collection')
|
35
|
+
collection.should_receive(:dup) \
|
36
|
+
.and_return(:mental_model_collection_dup)
|
37
|
+
configuration.stub!(:mental_model => stub(:foos => collection))
|
38
|
+
k.get_data(:foos).should == :mental_model_collection_dup
|
45
39
|
end
|
46
40
|
end
|
47
41
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kookaburra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 67
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 23
|
9
|
+
- 0
|
10
|
+
version: 0.23.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- John Wilger
|
@@ -17,7 +17,7 @@ autorequire:
|
|
17
17
|
bindir: bin
|
18
18
|
cert_chain: []
|
19
19
|
|
20
|
-
date: 2012-
|
20
|
+
date: 2012-04-02 00:00:00 Z
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
23
23
|
prerelease: false
|
@@ -205,6 +205,7 @@ files:
|
|
205
205
|
- lib/kookaburra/given_driver.rb
|
206
206
|
- lib/kookaburra/json_api_driver.rb
|
207
207
|
- lib/kookaburra/mental_model.rb
|
208
|
+
- lib/kookaburra/mental_model_matcher.rb
|
208
209
|
- lib/kookaburra/test_helpers.rb
|
209
210
|
- lib/kookaburra/ui_driver.rb
|
210
211
|
- lib/kookaburra/ui_driver/ui_component.rb
|
@@ -213,7 +214,9 @@ files:
|
|
213
214
|
- spec/kookaburra/api_driver_spec.rb
|
214
215
|
- spec/kookaburra/configuration_spec.rb
|
215
216
|
- spec/kookaburra/json_api_driver_spec.rb
|
217
|
+
- spec/kookaburra/mental_model_matcher_spec.rb
|
216
218
|
- spec/kookaburra/mental_model_spec.rb
|
219
|
+
- spec/kookaburra/test_helpers_spec.rb
|
217
220
|
- spec/kookaburra/ui_driver/ui_component/address_bar_spec.rb
|
218
221
|
- spec/kookaburra/ui_driver/ui_component_spec.rb
|
219
222
|
- spec/kookaburra/ui_driver_spec.rb
|