kookaburra 1.3.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/README.markdown +52 -59
- data/lib/kookaburra.rb +11 -17
- data/lib/kookaburra/api_client.rb +214 -0
- data/lib/kookaburra/api_driver.rb +47 -196
- data/lib/kookaburra/configuration.rb +4 -4
- data/lib/kookaburra/mental_model.rb +16 -4
- data/lib/kookaburra/test_helpers.rb +15 -37
- data/lib/kookaburra/version.rb +1 -1
- data/spec/integration/test_a_rack_application_spec.rb +34 -356
- data/spec/kookaburra/{api_driver_spec.rb → api_client_spec.rb} +14 -14
- data/spec/kookaburra/configuration_spec.rb +6 -6
- data/spec/kookaburra/mental_model_spec.rb +13 -1
- data/spec/kookaburra/test_helpers_spec.rb +5 -44
- data/spec/kookaburra_spec.rb +7 -7
- data/spec/support/json_api_app_and_kookaburra_drivers.rb +372 -0
- metadata +41 -42
- data/lib/kookaburra/given_driver.rb +0 -65
- data/lib/kookaburra/mental_model_matcher.rb +0 -138
- data/spec/kookaburra/mental_model_matcher_spec.rb +0 -237
@@ -1,214 +1,65 @@
|
|
1
|
-
require '
|
2
|
-
require 'core_ext/object/to_query'
|
3
|
-
require 'kookaburra/exceptions'
|
1
|
+
require 'forwardable'
|
4
2
|
|
5
3
|
class Kookaburra
|
6
|
-
#
|
4
|
+
# Your APIDriver subclass is used to define your testing DSL for setting up
|
5
|
+
# test preconditions. Unlike {Kookaburra::APIClient}, which is meant to be a
|
6
|
+
# simple mapping to your application's API, a method in the APIDriver may be
|
7
|
+
# comprised of several distinct API calls as well as access to Kookaburra's
|
8
|
+
# test data store via {#mental_model}.
|
7
9
|
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
10
|
+
# @abstract Subclass and implement your Given DSL.
|
11
|
+
#
|
12
|
+
# @example APIDriver subclass
|
13
|
+
# module MyApp
|
14
|
+
# module Kookaburra
|
15
|
+
# class APIDriver < ::Kookaburra::APIDriver
|
16
|
+
# def api
|
17
|
+
# @api ||= APIClient.new(configuration)
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def a_widget(name, attributes = {})
|
21
|
+
# # Set up the data that will be passed to the API by merging any
|
22
|
+
# # passed attributes into the default data.
|
23
|
+
# data = {:name => 'Foo', :description => 'Bar baz'}.merge(attributes)
|
24
|
+
#
|
25
|
+
# # Call the API method and get the resulting response as Ruby data.
|
26
|
+
# result = api.create_widget(data)
|
27
|
+
#
|
28
|
+
# # Store the resulting widget data in the MentalModel object, so that
|
29
|
+
# # it can be referenced in other operations.
|
30
|
+
# mental_model.widgets[name] = result
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# end
|
16
35
|
class APIDriver
|
17
|
-
|
18
|
-
# Serializes input data
|
19
|
-
#
|
20
|
-
# If specified, any input data provided to {APIDriver#post},
|
21
|
-
# {APIDriver#put} or {APIDriver#request} will be processed through
|
22
|
-
# this function prior to being sent to the HTTP server.
|
23
|
-
#
|
24
|
-
# @yieldparam data [Object] The data parameter that was passed to
|
25
|
-
# the request method
|
26
|
-
# @yieldreturn [String] The text to be used as the request body
|
27
|
-
#
|
28
|
-
# @example
|
29
|
-
# class MyAPIDriver < Kookaburra::APIDriver
|
30
|
-
# encode_with { |data| JSON.dump(data) }
|
31
|
-
# # ...
|
32
|
-
# end
|
33
|
-
def encode_with(&block)
|
34
|
-
define_method(:encode) do |data|
|
35
|
-
return if data.nil?
|
36
|
-
block.call(data)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# Deserialize response body
|
41
|
-
#
|
42
|
-
# If specified, the response bodies of all requests made using
|
43
|
-
# this {APIDriver} will be processed through this function prior
|
44
|
-
# to being returned.
|
45
|
-
#
|
46
|
-
# @yieldparam data [String] The response body sent by the HTTP
|
47
|
-
# server
|
48
|
-
#
|
49
|
-
# @yieldreturn [Object] The result of parsing the response body
|
50
|
-
# through this function
|
51
|
-
#
|
52
|
-
# @example
|
53
|
-
# class MyAPIDriver < Kookaburra::APIDriver
|
54
|
-
# decode_with { |data| JSON.parse(data) }
|
55
|
-
# # ...
|
56
|
-
# end
|
57
|
-
def decode_with(&block)
|
58
|
-
define_method(:decode) do |data|
|
59
|
-
block.call(data)
|
60
|
-
end
|
61
|
-
end
|
36
|
+
extend Forwardable
|
62
37
|
|
63
|
-
|
64
|
-
|
65
|
-
# Can be called multiple times to set HTTP headers that will be
|
66
|
-
# provided with every request made by the {APIDriver}.
|
67
|
-
#
|
68
|
-
# @param [String] name The name of the header, e.g. 'Content-Type'
|
69
|
-
# @param [String] value The value to which the header is set
|
70
|
-
#
|
71
|
-
# @example
|
72
|
-
# class MyAPIDriver < Kookaburra::APIDriver
|
73
|
-
# header 'Content-Type', 'application/json'
|
74
|
-
# header 'Accept', 'application/json'
|
75
|
-
# # ...
|
76
|
-
# end
|
77
|
-
def header(name, value)
|
78
|
-
headers[name] = value
|
79
|
-
end
|
80
|
-
|
81
|
-
# Used to retrieve the list of headers within the instance. Not
|
82
|
-
# intended to be used elsewhere.
|
83
|
-
#
|
84
|
-
# @private
|
85
|
-
def headers
|
86
|
-
@headers ||= {}
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
# Create a new {APIDriver} instance
|
38
|
+
# It is unlikely that you would call #initialize yourself; your APIDriver
|
39
|
+
# object is instantiated for you by {Kookaburra#given}.
|
91
40
|
#
|
92
41
|
# @param [Kookaburra::Configuration] configuration
|
93
|
-
|
94
|
-
# overriden when testing Kookaburra itself
|
95
|
-
def initialize(configuration, http_client = RestClient)
|
42
|
+
def initialize(configuration)
|
96
43
|
@configuration = configuration
|
97
|
-
@http_client = http_client
|
98
44
|
end
|
99
45
|
|
100
|
-
|
101
|
-
#
|
102
|
-
# @see APIDriver#request
|
103
|
-
def post(path, data = nil, headers = {})
|
104
|
-
request(:post, path, data, headers)
|
105
|
-
end
|
46
|
+
protected
|
106
47
|
|
107
|
-
|
108
|
-
#
|
109
|
-
# @see APIDriver#request
|
110
|
-
def put(path, data = nil, headers = {})
|
111
|
-
request(:put, path, data, headers)
|
112
|
-
end
|
48
|
+
attr_reader :configuration
|
113
49
|
|
114
|
-
#
|
50
|
+
# Access to the shared {Kookaburra::MentalModel} instance
|
115
51
|
#
|
116
|
-
# @
|
117
|
-
|
118
|
-
path = add_querystring_to_path(path, data)
|
119
|
-
request(:get, path, nil, headers)
|
120
|
-
end
|
52
|
+
# @attribute [rw] mental_model
|
53
|
+
def_delegator :configuration, :mental_model
|
121
54
|
|
122
|
-
#
|
123
|
-
#
|
124
|
-
# @see APIDriver#request
|
125
|
-
def delete(path, data = nil, headers = {})
|
126
|
-
path = add_querystring_to_path(path, data)
|
127
|
-
request(:delete, path, nil, headers)
|
128
|
-
end
|
129
|
-
|
130
|
-
# Make an HTTP request
|
131
|
-
#
|
132
|
-
# If you need to make a request other than the typical GET, POST,
|
133
|
-
# PUT and DELETE, you can use this method directly.
|
134
|
-
#
|
135
|
-
# This *will* follow redirects when the server's response code is in
|
136
|
-
# the 3XX range. If the response is a 303, the request will be
|
137
|
-
# transformed into a GET request.
|
138
|
-
#
|
139
|
-
# @see APIDriver.encode_with
|
140
|
-
# @see APIDriver.decode_with
|
141
|
-
# @see APIDriver.header
|
142
|
-
# @see APIDriver#get
|
143
|
-
# @see APIDriver#post
|
144
|
-
# @see APIDriver#put
|
145
|
-
# @see APIDriver#delete
|
55
|
+
# Used to access your APIClient in your own APIDriver implementation
|
146
56
|
#
|
147
|
-
# @
|
148
|
-
# @
|
149
|
-
#
|
150
|
-
#
|
151
|
-
|
152
|
-
|
153
|
-
# long as the encoder can serialize it into a String. If no
|
154
|
-
# encoder was specified, then this can be one of:
|
155
|
-
#
|
156
|
-
# * a String - will be passed as is
|
157
|
-
# * a Hash - will be encoded as normal HTTP form params
|
158
|
-
# * a Hash containing references to one or more Files - will
|
159
|
-
# set the content type to multipart/form-data
|
160
|
-
#
|
161
|
-
# @return [Object] The response body returned by the server. If a
|
162
|
-
# decoder was specified, this will return the result of
|
163
|
-
# parsing the response body through the decoder function.
|
164
|
-
#
|
165
|
-
# @raise [Kookaburra::UnexpectedResponse] Raised if the HTTP
|
166
|
-
# response received is not in the 2XX-3XX range.
|
167
|
-
def request(method, path, data, headers)
|
168
|
-
data = encode(data)
|
169
|
-
headers = global_headers.merge(headers)
|
170
|
-
response = @http_client.send(method, url_for(path), *[data, headers].compact)
|
171
|
-
decode(response.body)
|
172
|
-
rescue RestClient::Exception => e
|
173
|
-
raise_unexpected_response(e)
|
174
|
-
end
|
175
|
-
|
176
|
-
private
|
177
|
-
|
178
|
-
def add_querystring_to_path(path, data)
|
179
|
-
return path if data.nil? || data == {}
|
180
|
-
"#{path}?#{data.to_query}"
|
181
|
-
end
|
182
|
-
|
183
|
-
def global_headers
|
184
|
-
self.class.headers
|
185
|
-
end
|
186
|
-
|
187
|
-
def url_for(path)
|
188
|
-
URI.join(base_url, path).to_s
|
189
|
-
end
|
190
|
-
|
191
|
-
def base_url
|
192
|
-
@configuration.app_host
|
193
|
-
end
|
194
|
-
|
195
|
-
def encode(data)
|
196
|
-
data
|
197
|
-
end
|
198
|
-
|
199
|
-
def decode(data)
|
200
|
-
data
|
201
|
-
end
|
202
|
-
|
203
|
-
def raise_unexpected_response(exception)
|
204
|
-
message = <<-END
|
205
|
-
Unexpected response from server: #{exception.message}
|
206
|
-
|
207
|
-
#{exception.http_body}
|
208
|
-
END
|
209
|
-
new_exception = UnexpectedResponse.new(message)
|
210
|
-
new_exception.set_backtrace(exception.backtrace)
|
211
|
-
raise new_exception
|
57
|
+
# @abstract
|
58
|
+
# @return [Kookaburra::APIClient]
|
59
|
+
# @raise [Kookaburra::ConfigurationError] raised if you do not provide an
|
60
|
+
# implementation.
|
61
|
+
def api
|
62
|
+
raise ConfigurationError, "You must implement #api in your subclass."
|
212
63
|
end
|
213
64
|
end
|
214
65
|
end
|
@@ -7,12 +7,12 @@ class Kookaburra
|
|
7
7
|
class Configuration
|
8
8
|
extend DependencyAccessor
|
9
9
|
|
10
|
-
# The class to use as your
|
10
|
+
# The class to use as your APIDriver
|
11
11
|
#
|
12
|
-
# @attribute [rw]
|
12
|
+
# @attribute [rw] api_driver_class
|
13
13
|
# @raise [Kookaburra::ConfigurationError] if you try to read this attribute
|
14
14
|
# without it having been set
|
15
|
-
dependency_accessor :
|
15
|
+
dependency_accessor :api_driver_class
|
16
16
|
|
17
17
|
# The class to use as your UIDriver
|
18
18
|
#
|
@@ -39,7 +39,7 @@ class Kookaburra
|
|
39
39
|
dependency_accessor :app_host
|
40
40
|
|
41
41
|
# This is the {Kookaburra::MentalModel} that is shared between your
|
42
|
-
#
|
42
|
+
# APIDriver and your UIDriver. This attribute is managed by {Kookaburra},
|
43
43
|
# so you shouldn't need to change it yourself.
|
44
44
|
#
|
45
45
|
# @attribute [rw] mental_model
|
@@ -4,7 +4,7 @@ require 'kookaburra/exceptions'
|
|
4
4
|
class Kookaburra
|
5
5
|
# Each instance of {Kookaburra} has its own instance of MentalModel. This object
|
6
6
|
# is used to maintain a shared understanding of the application state between
|
7
|
-
# your {
|
7
|
+
# your {APIDriver} and your {UIDriver}. You can access the various test data
|
8
8
|
# collections in your test implementations via {Kookaburra#get_data}.
|
9
9
|
#
|
10
10
|
# The mental model is not intended to represent a copy of all of the data
|
@@ -48,11 +48,13 @@ class Kookaburra
|
|
48
48
|
# # Raises an UnknownKeyError
|
49
49
|
# mental_model.widgets[:bar]
|
50
50
|
class Collection < SimpleDelegator
|
51
|
+
attr_reader :name
|
52
|
+
|
51
53
|
# @param [String] name The name of the collection. Used to provide
|
52
54
|
# helpful error messages when unknown keys are accessed.
|
53
55
|
# @param [Hash] init_data Preloads specific data into the collection
|
54
56
|
def initialize(name, init_data = nil)
|
55
|
-
|
57
|
+
self.name = name
|
56
58
|
data = Hash.new do |hash, key|
|
57
59
|
raise UnknownKeyError, "Can't find mental_model.#{@name}[#{key.inspect}]. Did you forget to set it?"
|
58
60
|
end
|
@@ -119,7 +121,7 @@ class Kookaburra
|
|
119
121
|
#
|
120
122
|
# @return [Kookaburra::MentalModel::Collection] the deleted items subcollection
|
121
123
|
def deleted
|
122
|
-
@deleted ||= self.class.new("deleted")
|
124
|
+
@deleted ||= self.class.new("#{name}.deleted")
|
123
125
|
end
|
124
126
|
|
125
127
|
# Deletes key/value pairs from the collection for which the given block evaluates
|
@@ -137,8 +139,18 @@ class Kookaburra
|
|
137
139
|
def dup
|
138
140
|
new_data = {}.merge(self)
|
139
141
|
new_data = Marshal.load(Marshal.dump(new_data))
|
140
|
-
self.class.new(@name, new_data)
|
142
|
+
self.class.new(@name, new_data).tap do |mm|
|
143
|
+
mm.deleted = deleted.dup unless deleted.empty?
|
144
|
+
end
|
141
145
|
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
attr_writer :deleted
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
attr_writer :name
|
142
154
|
end
|
143
155
|
end
|
144
156
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
require 'kookaburra'
|
3
|
-
require 'kookaburra/mental_model_matcher'
|
4
3
|
|
5
4
|
class Kookaburra
|
6
5
|
# This module is intended to be mixed in to your testing context to provide
|
@@ -11,12 +10,12 @@ class Kookaburra
|
|
11
10
|
# @example RSpec setup
|
12
11
|
# # in 'spec/support/kookaburra_setup.rb'
|
13
12
|
# require 'kookaburra/test_helpers'
|
14
|
-
# require 'my_app/kookaburra/
|
13
|
+
# require 'my_app/kookaburra/api_driver'
|
15
14
|
# require 'my_app/kookaburra/ui_driver'
|
16
|
-
#
|
15
|
+
#
|
17
16
|
# Kookaburra.configure do |c|
|
18
|
-
# c.
|
19
|
-
# c.ui_driver_class =
|
17
|
+
# c.api_driver_class = MyApp::Kookaburra::APIDriver,
|
18
|
+
# c.ui_driver_class = MyApp::Kookaburra::UIDriver,
|
20
19
|
# c.app_host = 'http://my_app.example.com:12345',
|
21
20
|
# c.browser = capybara,
|
22
21
|
# c.server_error_detection { |browser|
|
@@ -31,9 +30,9 @@ class Kookaburra
|
|
31
30
|
# # in 'spec/request/some_feature_spec.rb'
|
32
31
|
# describe "Some Feature" do
|
33
32
|
# example "some test script" do
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
33
|
+
# api.create_widget(:foo)
|
34
|
+
# api.create_widget(:bar, :hidden => true)
|
35
|
+
# api.create_widget(:baz)
|
37
36
|
#
|
38
37
|
# ui.view_list_of_widgets
|
39
38
|
#
|
@@ -48,12 +47,12 @@ class Kookaburra
|
|
48
47
|
# @example Cucumber setup
|
49
48
|
# # in 'features/support/kookaburra_setup.rb'
|
50
49
|
# require 'kookaburra/test_helpers'
|
51
|
-
# require 'my_app/kookaburra/
|
50
|
+
# require 'my_app/kookaburra/api_driver'
|
52
51
|
# require 'my_app/kookaburra/ui_driver'
|
53
|
-
#
|
52
|
+
#
|
54
53
|
# Kookaburra.configure do |c|
|
55
|
-
# c.
|
56
|
-
# c.ui_driver_class =
|
54
|
+
# c.api_driver_class = MyApp::Kookaburra::APIDriver,
|
55
|
+
# c.ui_driver_class = MyApp::Kookaburra::UIDriver,
|
57
56
|
# c.app_host = 'http://my_app.example.com:12345',
|
58
57
|
# c.browser = capybara,
|
59
58
|
# c.server_error_detection { |browser|
|
@@ -65,11 +64,11 @@ class Kookaburra
|
|
65
64
|
#
|
66
65
|
# # in 'features/step_definitions/some_steps.rb
|
67
66
|
# Given /^there is a widget, "(\w+)"/ do |name|
|
68
|
-
#
|
67
|
+
# api.create_widget(name.to_sym)
|
69
68
|
# end
|
70
69
|
#
|
71
70
|
# Given /^there is a hidden widget, "(\w+)"/ do |name|
|
72
|
-
#
|
71
|
+
# api.create_widget(name.to_sym, :hidden => true)
|
73
72
|
# end
|
74
73
|
#
|
75
74
|
# When /^I view the widget list/ do
|
@@ -92,33 +91,12 @@ class Kookaburra
|
|
92
91
|
@k ||= Kookaburra.new
|
93
92
|
end
|
94
93
|
|
95
|
-
# @method
|
94
|
+
# @method api
|
96
95
|
# Delegates to {#k}
|
97
|
-
def_delegator :k, :
|
96
|
+
def_delegator :k, :api
|
98
97
|
|
99
98
|
# @method ui
|
100
99
|
# Delegates to {#k}
|
101
100
|
def_delegator :k, :ui
|
102
|
-
|
103
|
-
# RSpec-style custom matcher that compares a given array with
|
104
|
-
# the current state of one named collection in the mental model
|
105
|
-
#
|
106
|
-
# @see Kookaburra::MentalModel::Matcher
|
107
|
-
def match_mental_model_of(collection_key)
|
108
|
-
MentalModel::Matcher.new(k.send(:__mental_model__), collection_key)
|
109
|
-
end
|
110
|
-
|
111
|
-
# Custom assertion for Test::Unit-style tests
|
112
|
-
# (really, anything that uses #assert(predicate, message = nil))
|
113
|
-
#
|
114
|
-
# @see Kookaburra::MentalModel::Matcher
|
115
|
-
def assert_mental_model_matches(collection_key, actual, message = nil)
|
116
|
-
matcher = match_mental_model_of(collection_key)
|
117
|
-
result = matcher.matches?(actual)
|
118
|
-
return if !!result # don't even bother
|
119
|
-
|
120
|
-
message ||= matcher.failure_message_for_should
|
121
|
-
assert result, message
|
122
|
-
end
|
123
101
|
end
|
124
102
|
end
|
data/lib/kookaburra/version.rb
CHANGED
@@ -1,352 +1,17 @@
|
|
1
1
|
require 'kookaburra/test_helpers'
|
2
|
-
require 'kookaburra/
|
2
|
+
require 'kookaburra/api_client'
|
3
3
|
require 'kookaburra/rack_app_server'
|
4
4
|
require 'capybara'
|
5
5
|
require 'capybara/webkit'
|
6
6
|
require 'uuid'
|
7
7
|
|
8
|
-
|
9
|
-
require 'sinatra/base'
|
10
|
-
require 'json'
|
8
|
+
require 'support/json_api_app_and_kookaburra_drivers'
|
11
9
|
|
12
10
|
describe "testing a Rack application with Kookaburra" do
|
13
11
|
include Kookaburra::TestHelpers
|
14
12
|
|
15
13
|
describe "with an HTML interface" do
|
16
14
|
describe "with a JSON API" do
|
17
|
-
# This is the fixture Rack application against which the integration
|
18
|
-
# test will run. It uses class variables to persist data, because
|
19
|
-
# Sinatra will instantiate a new instance of TestRackApp for each
|
20
|
-
# request.
|
21
|
-
class JsonApiApp < Sinatra::Base
|
22
|
-
enable :sessions
|
23
|
-
disable :show_exceptions
|
24
|
-
|
25
|
-
def parse_json_req_body
|
26
|
-
request.body.rewind
|
27
|
-
JSON.parse(request.body.read, :symbolize_names => true)
|
28
|
-
end
|
29
|
-
|
30
|
-
post '/users' do
|
31
|
-
user_data = parse_json_req_body
|
32
|
-
@@users ||= {}
|
33
|
-
@@users[user_data[:email]] = user_data
|
34
|
-
status 201
|
35
|
-
headers 'Content-Type' => 'application/json'
|
36
|
-
body user_data.to_json
|
37
|
-
end
|
38
|
-
|
39
|
-
post '/session' do
|
40
|
-
user = @@users[params[:email]]
|
41
|
-
if user && user[:password] == params[:password]
|
42
|
-
session[:logged_in] = true
|
43
|
-
status 200
|
44
|
-
body 'You are logged in!'
|
45
|
-
else
|
46
|
-
session[:logged_in] = false
|
47
|
-
status 403
|
48
|
-
body 'Log in failed!'
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
get '/session/new' do
|
53
|
-
body <<-EOF
|
54
|
-
<html>
|
55
|
-
<head>
|
56
|
-
<title>Sign In</title>
|
57
|
-
</head>
|
58
|
-
<body>
|
59
|
-
<div id="sign_in_screen">
|
60
|
-
<form action="/session" method="POST">
|
61
|
-
<label for="email">Email:</label>
|
62
|
-
<input id="email" name="email" type="text" />
|
63
|
-
|
64
|
-
<label for="password">Password:</label>
|
65
|
-
<input id="password" name="password" type="password" />
|
66
|
-
|
67
|
-
<input type="submit" value="Sign In" />
|
68
|
-
</form>
|
69
|
-
</div>
|
70
|
-
</body>
|
71
|
-
</html>
|
72
|
-
EOF
|
73
|
-
end
|
74
|
-
|
75
|
-
post '/widgets/:widget_id' do
|
76
|
-
@@widgets.delete_if do |w|
|
77
|
-
w[:id] == params['widget_id']
|
78
|
-
end
|
79
|
-
redirect to('/widgets')
|
80
|
-
end
|
81
|
-
|
82
|
-
get '/widgets/new' do
|
83
|
-
body <<-EOF
|
84
|
-
<html>
|
85
|
-
<head>
|
86
|
-
<title>New Widget</title>
|
87
|
-
</head>
|
88
|
-
<body>
|
89
|
-
<div id="widget_form">
|
90
|
-
<form action="/widgets" method="POST">
|
91
|
-
<label for="name">Name:</label>
|
92
|
-
<input id="name" name="name" type="text" />
|
93
|
-
|
94
|
-
<input type="submit" value="Save" />
|
95
|
-
</form>
|
96
|
-
</div>
|
97
|
-
</body>
|
98
|
-
</html>
|
99
|
-
EOF
|
100
|
-
end
|
101
|
-
|
102
|
-
post '/widgets' do
|
103
|
-
@@widgets ||= []
|
104
|
-
widget_data = if request.media_type == 'application/json'
|
105
|
-
parse_json_req_body
|
106
|
-
else
|
107
|
-
{:name => params['name']}
|
108
|
-
end
|
109
|
-
widget_data[:id] = UUID.new.generate
|
110
|
-
@@widgets << widget_data
|
111
|
-
@@last_widget_created = widget_data
|
112
|
-
request.accept.each do |type|
|
113
|
-
case type.to_s
|
114
|
-
when 'application/json'
|
115
|
-
status 201
|
116
|
-
headers 'Content-Type' => 'application/json'
|
117
|
-
halt widget_data.to_json
|
118
|
-
else
|
119
|
-
halt redirect to('/widgets')
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
get '/widgets' do
|
125
|
-
raise "Not logged in!" unless session[:logged_in]
|
126
|
-
@@widgets ||= []
|
127
|
-
last_widget_created, @@last_widget_created = @@last_widget_created, nil
|
128
|
-
content = ''
|
129
|
-
content << <<-EOF
|
130
|
-
<html>
|
131
|
-
<head>
|
132
|
-
<title>Widgets</title>
|
133
|
-
</head>
|
134
|
-
<body>
|
135
|
-
<div id="widget_list">
|
136
|
-
EOF
|
137
|
-
if last_widget_created
|
138
|
-
content << <<-EOF
|
139
|
-
<div class="last_widget created">
|
140
|
-
<span class="id">#{last_widget_created[:id]}</span>
|
141
|
-
<span class="name">#{last_widget_created[:name]}</span>
|
142
|
-
</div>
|
143
|
-
EOF
|
144
|
-
end
|
145
|
-
content << <<-EOF
|
146
|
-
<ul>
|
147
|
-
EOF
|
148
|
-
@@widgets.each do |w|
|
149
|
-
content << <<-EOF
|
150
|
-
<li class="widget_summary">
|
151
|
-
<span class="id">#{w[:id]}</span>
|
152
|
-
<span class="name">#{w[:name]}</span>
|
153
|
-
<form id="delete_#{w[:id]}" action="/widgets/#{w[:id]}" method="POST">
|
154
|
-
<button type="submit" value="Delete" />
|
155
|
-
</form>
|
156
|
-
</li>
|
157
|
-
EOF
|
158
|
-
end
|
159
|
-
content << <<-EOF
|
160
|
-
</ul>
|
161
|
-
<a href="/widgets/new">New Widget</a>
|
162
|
-
</div>
|
163
|
-
</body>
|
164
|
-
</html>
|
165
|
-
EOF
|
166
|
-
body content
|
167
|
-
end
|
168
|
-
|
169
|
-
get '/error_page' do
|
170
|
-
content = <<-EOF
|
171
|
-
<html>
|
172
|
-
<head>
|
173
|
-
<title>Internal Server Error</title>
|
174
|
-
</head>
|
175
|
-
<body>
|
176
|
-
<p>A Purposeful Error</p>
|
177
|
-
</body>
|
178
|
-
</html>
|
179
|
-
EOF
|
180
|
-
body content
|
181
|
-
end
|
182
|
-
|
183
|
-
error do
|
184
|
-
e = request.env['sinatra.error']
|
185
|
-
body << <<-EOF
|
186
|
-
<html>
|
187
|
-
<head>
|
188
|
-
<title>Internal Server Error</title>
|
189
|
-
</head>
|
190
|
-
<body>
|
191
|
-
<pre>
|
192
|
-
#{e.to_s}\n#{e.backtrace.join("\n")}
|
193
|
-
</pre>
|
194
|
-
</body>
|
195
|
-
</html>
|
196
|
-
EOF
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
class MyAPIDriver < Kookaburra::APIDriver
|
201
|
-
encode_with { |data| JSON.dump(data) }
|
202
|
-
decode_with { |data| JSON.parse(data) }
|
203
|
-
header 'Content-Type', 'application/json'
|
204
|
-
header 'Accept', 'application/json'
|
205
|
-
|
206
|
-
def create_user(user_data)
|
207
|
-
post '/users', user_data
|
208
|
-
end
|
209
|
-
|
210
|
-
def create_widget(widget_data)
|
211
|
-
post '/widgets', widget_data
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
class MyGivenDriver < Kookaburra::GivenDriver
|
216
|
-
def api
|
217
|
-
MyAPIDriver.new(configuration)
|
218
|
-
end
|
219
|
-
|
220
|
-
def a_user(name)
|
221
|
-
user = {'email' => 'bob@example.com', 'password' => '12345'}
|
222
|
-
result = api.create_user(user)
|
223
|
-
mental_model.users[name] = result
|
224
|
-
end
|
225
|
-
|
226
|
-
def a_widget(name, attributes = {})
|
227
|
-
widget = {'name' => 'Foo'}.merge(attributes)
|
228
|
-
result = api.create_widget(widget)
|
229
|
-
mental_model.widgets[name] = result
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
class SignInScreen < Kookaburra::UIDriver::UIComponent
|
234
|
-
def component_path
|
235
|
-
'/session/new'
|
236
|
-
end
|
237
|
-
|
238
|
-
# Use default component locator value
|
239
|
-
#
|
240
|
-
# def component_locator
|
241
|
-
# '#sign_in_screen'
|
242
|
-
# end
|
243
|
-
|
244
|
-
def sign_in(user_data)
|
245
|
-
fill_in 'Email:', :with => user_data['email']
|
246
|
-
fill_in 'Password:', :with => user_data['password']
|
247
|
-
click_button 'Sign In'
|
248
|
-
end
|
249
|
-
end
|
250
|
-
|
251
|
-
class ErrorPage < Kookaburra::UIDriver::UIComponent
|
252
|
-
def component_path
|
253
|
-
'/error_page'
|
254
|
-
end
|
255
|
-
end
|
256
|
-
|
257
|
-
class WidgetDataContainer
|
258
|
-
def initialize(element)
|
259
|
-
@element = element
|
260
|
-
end
|
261
|
-
|
262
|
-
def to_hash
|
263
|
-
{
|
264
|
-
'id' => @element.find('.id').text,
|
265
|
-
'name' => @element.find('.name').text
|
266
|
-
}
|
267
|
-
end
|
268
|
-
end
|
269
|
-
|
270
|
-
class LastWidgetCreated < Kookaburra::UIDriver::UIComponent
|
271
|
-
def component_locator
|
272
|
-
@options[:component_locator]
|
273
|
-
end
|
274
|
-
|
275
|
-
def data
|
276
|
-
raise "Foo" unless visible?
|
277
|
-
WidgetDataContainer.new(self).to_hash
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
class WidgetList < Kookaburra::UIDriver::UIComponent
|
282
|
-
ui_component :last_widget_created, LastWidgetCreated, :component_locator => '#widget_list .last_widget.created'
|
283
|
-
|
284
|
-
def component_path
|
285
|
-
'/widgets'
|
286
|
-
end
|
287
|
-
|
288
|
-
def component_locator
|
289
|
-
'#widget_list'
|
290
|
-
end
|
291
|
-
|
292
|
-
def widgets
|
293
|
-
all('.widget_summary').map do |el|
|
294
|
-
WidgetDataContainer.new(el).to_hash
|
295
|
-
end
|
296
|
-
end
|
297
|
-
|
298
|
-
def choose_to_create_new_widget
|
299
|
-
click_on 'New Widget'
|
300
|
-
end
|
301
|
-
|
302
|
-
def choose_to_delete_widget(widget_data)
|
303
|
-
find("#delete_#{widget_data['id']}").click_button('Delete')
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
class WidgetForm < Kookaburra::UIDriver::UIComponent
|
308
|
-
def component_locator
|
309
|
-
'#widget_form'
|
310
|
-
end
|
311
|
-
|
312
|
-
def submit(widget_data)
|
313
|
-
fill_in 'Name:', :with => widget_data['name']
|
314
|
-
click_on 'Save'
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
class MyUIDriver < Kookaburra::UIDriver
|
319
|
-
ui_component :error_page, ErrorPage
|
320
|
-
ui_component :sign_in_screen, SignInScreen
|
321
|
-
ui_component :widget_list, WidgetList
|
322
|
-
ui_component :widget_form, WidgetForm
|
323
|
-
|
324
|
-
def sign_in(name)
|
325
|
-
address_bar.go_to sign_in_screen
|
326
|
-
sign_in_screen.sign_in(mental_model.users[name])
|
327
|
-
end
|
328
|
-
|
329
|
-
def error_on_purpose
|
330
|
-
address_bar.go_to error_page
|
331
|
-
end
|
332
|
-
|
333
|
-
def view_widget_list
|
334
|
-
address_bar.go_to widget_list
|
335
|
-
end
|
336
|
-
|
337
|
-
def create_new_widget(name, attributes = {})
|
338
|
-
assert widget_list.visible?, "Widget list is not visible!"
|
339
|
-
widget_list.choose_to_create_new_widget
|
340
|
-
widget_form.submit('name' => 'My Widget')
|
341
|
-
mental_model.widgets[name] = widget_list.last_widget_created.data
|
342
|
-
end
|
343
|
-
|
344
|
-
def delete_widget(name)
|
345
|
-
assert widget_list.visible?, "Widget list is not visible!"
|
346
|
-
widget_list.choose_to_delete_widget(mental_model.widgets.delete(name))
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
15
|
app_server = Kookaburra::RackAppServer.new do
|
351
16
|
JsonApiApp.new
|
352
17
|
end
|
@@ -356,7 +21,7 @@ describe "testing a Rack application with Kookaburra" do
|
|
356
21
|
|
357
22
|
Kookaburra.configure do |c|
|
358
23
|
c.ui_driver_class = MyUIDriver
|
359
|
-
c.
|
24
|
+
c.api_driver_class = MyAPIDriver
|
360
25
|
c.app_host = 'http://127.0.0.1:%d' % app_server.port
|
361
26
|
c.browser = Capybara::Session.new(:webkit)
|
362
27
|
c.server_error_detection do |browser|
|
@@ -369,32 +34,45 @@ describe "testing a Rack application with Kookaburra" do
|
|
369
34
|
app_server.shutdown
|
370
35
|
end
|
371
36
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
37
|
+
before(:each) do
|
38
|
+
api.create_user(:bob)
|
39
|
+
api.create_widget(:widget_a)
|
40
|
+
api.create_widget(:widget_b, :name => 'Widget B')
|
41
|
+
end
|
42
|
+
|
43
|
+
define_method(:widgets) { k.get_data(:widgets) }
|
376
44
|
|
45
|
+
it "runs the tests against the application's UI" do
|
377
46
|
ui.sign_in(:bob)
|
378
|
-
ui.view_widget_list
|
379
47
|
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_b)
|
384
|
-
ui.widget_list.widgets.should match_mental_model_of(:widgets)
|
48
|
+
ui.view_widget_list
|
49
|
+
expect(ui.widget_list.widgets).to include widgets[:widget_a]
|
50
|
+
expect(ui.widget_list.widgets).to include widgets[:widget_b]
|
385
51
|
|
386
|
-
ui.
|
52
|
+
ui.create_widget(:widget_c, :name => 'Bar')
|
53
|
+
expect(ui.widget_list.widgets).to include widgets[:widget_a]
|
54
|
+
expect(ui.widget_list.widgets).to include widgets[:widget_b]
|
55
|
+
expect(ui.widget_list.widgets).to include widgets[:widget_c]
|
387
56
|
|
57
|
+
ui.delete_widget(:widget_b)
|
58
|
+
expect(ui.widget_list.widgets).to include widgets[:widget_a]
|
59
|
+
expect(ui.widget_list.widgets).to include widgets[:widget_c]
|
60
|
+
expect(ui.widget_list.widgets).to_not include widgets.deleted[:widget_b]
|
61
|
+
end
|
388
62
|
|
389
|
-
|
390
|
-
|
391
|
-
|
63
|
+
it "runs the tests against the applications's API" do
|
64
|
+
expect(api.widgets).to include widgets[:widget_a]
|
65
|
+
expect(api.widgets).to include widgets[:widget_b]
|
392
66
|
|
393
|
-
|
67
|
+
api.create_widget(:widget_c, :name => 'Bar')
|
68
|
+
expect(api.widgets).to include widgets[:widget_a]
|
69
|
+
expect(api.widgets).to include widgets[:widget_b]
|
70
|
+
expect(api.widgets).to include widgets[:widget_c]
|
394
71
|
|
395
|
-
|
396
|
-
|
397
|
-
|
72
|
+
api.delete_widget(:widget_b)
|
73
|
+
expect(api.widgets).to include widgets[:widget_a]
|
74
|
+
expect(api.widgets).to include widgets[:widget_c]
|
75
|
+
expect(api.widgets).to_not include widgets.deleted[:widget_b]
|
398
76
|
end
|
399
77
|
|
400
78
|
it "catches errors based on the server error detection handler" do
|