kookaburra 0.18.3 → 0.20.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/lib/kookaburra.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'kookaburra/exceptions'
2
- require 'kookaburra/test_data'
2
+ require 'kookaburra/mental_model'
3
3
  require 'kookaburra/given_driver'
4
4
  require 'kookaburra/ui_driver'
5
5
 
@@ -29,33 +29,33 @@ class Kookaburra
29
29
  # Returns a new Kookaburra instance that wires together your application's
30
30
  # APIDriver, GivenDriver, and UIDriver.
31
31
  #
32
- # @option options [Class] :api_driver_class Your application's
33
- # subclass of {Kookaburra::APIDriver}. At the moment, only the
34
- # {Kookaburra::JsonApiDriver} is implemented
35
32
  # @option options [Class] :given_driver_class Your
36
33
  # application's subclass of {Kookaburra::GivenDriver}
37
34
  # @option options [Class] :ui_driver_class Your application's
38
35
  # subclass of {Kookaburra::UIDriver}
39
- # @option options [Capybara::Session] :browser (optional) The browser driver
36
+ # @option options [Capybara::Session] :browser The browser driver
40
37
  # that Kookaburra will interact with to run the tests.
41
- # @option options [#call] :rack_app (optional) The Rack application to test
42
- # against.
38
+ # @option options [String] :app_host The URL of your running application
39
+ # server against which the tests will be run (e.g.
40
+ # "http://my_app.example.com:12345")
41
+ # @option options [Proc] :server_error_detection A proc that will receive the
42
+ # object passed in to the :browser option as an argument and must return
43
+ # `true` if the server responded with an unexpected error or `false` if it
44
+ # did not.
43
45
  def initialize(options = {})
44
- @api_driver_class = options[:api_driver_class]
45
46
  @given_driver_class = options[:given_driver_class]
46
47
  @ui_driver_class = options[:ui_driver_class]
47
48
  @browser = options[:browser]
48
- @rack_app = options[:rack_app]
49
+ @app_host = options[:app_host]
49
50
  @server_error_detection = options[:server_error_detection]
50
51
  end
51
52
 
52
53
  # Returns an instance of your GivenDriver class configured to share test
53
- # fixture data with the UIDriver and to use your APIDriver class to
54
- # communicate with your application
54
+ # fixture data with the UIDriver
55
55
  #
56
56
  # @return [Kookaburra::GivenDriver]
57
57
  def given
58
- given_driver_class.new(:test_data => test_data, :api => api)
58
+ given_driver_class.new(:mental_model => mental_model, :app_host => @app_host)
59
59
  end
60
60
 
61
61
  # Returns an instance of your UIDriver class configured to share test fixture
@@ -64,18 +64,19 @@ class Kookaburra
64
64
  #
65
65
  # @return [Kookaburra::UIDriver]
66
66
  def ui
67
- ui_driver_class.new(:test_data => test_data,
67
+ ui_driver_class.new(:mental_model => mental_model,
68
68
  :browser => browser,
69
+ :app_host => @app_host,
69
70
  :server_error_detection => @server_error_detection)
70
71
  end
71
72
 
72
- # Returns a frozen copy of the specified test fixture data collection.
73
+ # Returns a frozen copy of the specified {MentalModel::Collection}.
73
74
  # However, this is neither a deep copy nor a deep freeze, so it is possible
74
75
  # that you could modify data outside of your GivenDriver or UIDriver. Just
75
76
  # don't do that. Trust me.
76
77
  #
77
- # This access is provided so that you can reference the current fixture data
78
- # within your test implementation in order to make assertions about the state
78
+ # This access is provided so that you can reference the current mental model
79
+ # within your test implementation and make assertions about the state
79
80
  # of your application's interface.
80
81
  #
81
82
  # @example
@@ -83,28 +84,18 @@ class Kookaburra
83
84
  # ui.create_a_new_widget(:bar)
84
85
  # ui.widget_list.widgets.should == k.get_data(:widgets).slice(:foo, :bar)
85
86
  #
86
- # @return [Kookaburra::TestData::Collection]
87
+ # @return [Kookaburra::MentalModel::Collection]
87
88
  def get_data(collection_name)
88
- test_data.send(collection_name).dup.freeze
89
+ mental_model.send(collection_name).dup.freeze
89
90
  end
90
91
 
91
92
  private
92
93
 
93
94
  extend DependencyAccessor
94
- dependency_accessor :given_driver_class, :api_driver_class, :ui_driver_class
95
+ dependency_accessor :given_driver_class, :ui_driver_class
95
96
 
96
- def api
97
- api_driver_class.new(application_driver)
98
- end
99
-
100
- def application_driver
101
- unless @rack_app.nil?
102
- RackDriver.new(@rack_app)
103
- end
104
- end
105
-
106
- def test_data
107
- @test_data ||= TestData.new
97
+ def mental_model
98
+ @mental_model ||= MentalModel.new
108
99
  end
109
100
 
110
101
  def browser
@@ -1,16 +1,100 @@
1
+ require 'kookaburra/exceptions'
2
+ require 'delegate'
3
+ require 'patron'
4
+
1
5
  class Kookaburra
2
- # This class currently exists only so that, in documentation, we can refer to
3
- # the generic APIDriver rather than the specific {Kookaburra::JsonApiDriver},
4
- # which is currently the only implementation. Once another APIDriver
5
- # implementation is added to Kookaburra, anything it has in common with
6
- # {Kookaburra::JsonApiDriver} should be factored up into this class.
7
- #
8
- # @abstract Subclass and provide an API client implementation
9
- class APIDriver
10
- # Returns a new APIDriver.
6
+ class APIDriver < SimpleDelegator
7
+ # Wraps `:http_client` in a `SimpleDelegator` that causes request methods to
8
+ # either return the response body or raise an exception on an unexpected
9
+ # response status code.
11
10
  #
12
- # @param args Not actually used, but takes any arguments
13
- def initialize(*args)
11
+ # @option options [String] :app_host The root URL of your running
12
+ # application (e.g. "http://my_app.example.com:12345")
13
+ # @option options [Patron::Session] :http_client (Patron::Session.new) The
14
+ # object responsible for actually making HTTP calls to your application.
15
+ def initialize(options = {})
16
+ http_client = options[:http_client] || Patron::Session.new
17
+ http_client.base_url = options[:app_host] if options.has_key?(:app_host)
18
+ super(http_client)
19
+ end
20
+
21
+ # Makes a POST request via the `:http_client`
22
+ #
23
+ # @param [String] path The path to request (e.g. "/foo")
24
+ # @param [Hash, String] data The post data. If a Hash is provided, it will
25
+ # be converted to an 'application/x-www-form-urlencoded' post body.
26
+ # @option options [Integer] :expected_response_status (201) The HTTP status
27
+ # code that you expect the server to respond with.
28
+ # @raise [Kookaburra::UnexpectedResponse] raised if the HTTP status of the
29
+ # response does not match the `:expected_response_status`
30
+ def post(path, data, options = {})
31
+ request(:post, path, options, data)
32
+ end
33
+
34
+ # Makes a PUT request via the `:http_client`
35
+ #
36
+ # @param [String] path The path to request (e.g. "/foo")
37
+ # @param [Hash, String] data The post data. If a Hash is provided, it will
38
+ # be converted to an 'application/x-www-form-urlencoded' post body.
39
+ # @option options [Integer] :expected_response_status (201) The HTTP status
40
+ # code that you expect the server to respond with.
41
+ # @raise [Kookaburra::UnexpectedResponse] raised if the HTTP status of the
42
+ # response does not match the `:expected_response_status`
43
+ def put(path, data, options = {})
44
+ request(:put, path, options, data)
45
+ end
46
+
47
+ # Makes a GET request via the `:http_client`
48
+ #
49
+ # @param [String] path The path to request (e.g. "/foo")
50
+ # @option options [Integer] :expected_response_status (201) The HTTP status
51
+ # code that you expect the server to respond with.
52
+ # @raise [Kookaburra::UnexpectedResponse] raised if the HTTP status of the
53
+ # response does not match the `:expected_response_status`
54
+ def get(path, options = {})
55
+ request(:get, path, options)
56
+ end
57
+
58
+ # Makes a DELETE request via the `:http_client`
59
+ #
60
+ # @param [String] path The path to request (e.g. "/foo")
61
+ # @option options [Integer] :expected_response_status (201) The HTTP status
62
+ # code that you expect the server to respond with.
63
+ # @raise [Kookaburra::UnexpectedResponse] raised if the HTTP status of the
64
+ # response does not match the `:expected_response_status`
65
+ def delete(path, options = {})
66
+ request(:delete, path, options)
67
+ end
68
+
69
+ private
70
+
71
+ def request(type, path, options = {}, data = nil)
72
+ # don't send a data argument if it's not passed in, because some methods
73
+ # on target object may not have the proper arity (i.e. #get and #delete).
74
+ args = [type, path, data, options].compact
75
+ response = __getobj__.send(*args)
76
+
77
+ check_response_status!(type, response, options)
78
+ response.body
79
+ end
80
+
81
+ def check_response_status!(request_type, response, options)
82
+ verb, default_status = verb_map[request_type]
83
+ expected_status = options[:expected_response_status] || default_status
84
+ unless expected_status == response.status
85
+ raise UnexpectedResponse, "#{verb} to #{response.url} responded with " \
86
+ + "#{response.status} status, not #{expected_status} as expected\n\n" \
87
+ + response.body
88
+ end
89
+ end
90
+
91
+ def verb_map
92
+ {
93
+ :get => ['GET', 200],
94
+ :post => ['POST', 201],
95
+ :put => ['PUT', 200],
96
+ :delete => ['DELETE', 200]
97
+ }
14
98
  end
15
99
  end
16
100
  end
@@ -7,12 +7,17 @@ class Kookaburra
7
7
  # comprised of several distinct API calls as well as access to Kookaburra's
8
8
  # test data store.
9
9
  #
10
- # @abstract Subclass and implement your Given DSL
10
+ # @abstract Subclass and implement your Given DSL. You must also provide an
11
+ # implementation of #api that returns an instance of your APIDriver.
11
12
  #
12
13
  # @example GivenDriver subclass
13
14
  # module MyApp
14
15
  # module Kookaburra
15
16
  # class GivenDriver < ::Kookaburra::GivenDriver
17
+ # def api
18
+ # @api ||= APIDriver.new(:app_host => initialization_options[:app_host])
19
+ # end
20
+ #
16
21
  # def a_widget(name, attributes = {})
17
22
  # # Set up the data that will be passed to the API by merging any
18
23
  # # passed attributes into the default data.
@@ -21,9 +26,9 @@ class Kookaburra
21
26
  # # Call the API method and get the resulting response as Ruby data.
22
27
  # result = api.create_widget(data)
23
28
  #
24
- # # Store the resulting widget data in the TestData object, so that
29
+ # # Store the resulting widget data in the MentalModel object, so that
25
30
  # # it can be referenced in other operations.
26
- # test_data.widgets[name] = result
31
+ # mental_model.widgets[name] = result
27
32
  # end
28
33
  # end
29
34
  # end
@@ -34,27 +39,38 @@ class Kookaburra
34
39
  # It is unlikely that you would call #initialize yourself; your GivenDriver
35
40
  # object is instantiated for you by {Kookaburra#given}.
36
41
  #
37
- # @option options [Kookaburra::TestData] the test data store
38
- # @option options [Kookaburra::APIDriver] the APIDriver subclass to be used
42
+ # @option options [Kookaburra::MentalModel] :mental_model the MentalModel
43
+ # instance used by your tests
44
+ # @option options [String] :app_host The root URL of your running
45
+ # application (e.g. "http://my_app.example.com:12345")
39
46
  def initialize(options = {})
40
- @test_data = options[:test_data]
41
- @api = options[:api]
47
+ @initialization_options = options
48
+ @mental_model = options[:mental_model]
42
49
  end
43
50
 
44
51
  protected
45
52
 
46
- # A reference to the {Kookaburra::TestData} object that this GivenDriver
47
- # instance was created with.
53
+ # Used to access your APIDriver in your own GivenDriver implementation
48
54
  #
49
- # @attribute [r]
50
- # @return [Kookaburra::TestData]
51
- dependency_accessor :test_data
55
+ # @abstract
56
+ # @return [Kookaburra::APIDriver]
57
+ # @raise [Kookaburra::ConfigurationError] raised if you do not provide an
58
+ # implementation.
59
+ def api
60
+ raise ConfigurationError, "You must implement #api in your subclass."
61
+ end
52
62
 
53
- # A reference to the {Kookaburra::APIDriver} that this GivenDriver instance
54
- # was created with.
63
+ # The full set of options passed in to {#initialize}
64
+ #
65
+ # Access is provided so that you can use these when instantiating your
66
+ # {APIDriver} in your {#api} implementation.
67
+ attr_reader :initialization_options
68
+
69
+ # A reference to the {Kookaburra::MentalModel} object that this GivenDriver
70
+ # instance was created with.
55
71
  #
56
72
  # @attribute [r]
57
- # @return [Kookaburra::APIDriver]
58
- dependency_accessor :api
73
+ # @return [Kookaburra::MentalModel]
74
+ dependency_accessor :mental_model
59
75
  end
60
76
  end
@@ -1,90 +1,63 @@
1
- require 'kookaburra/dependency_accessor'
2
- require 'kookaburra/rack_driver'
3
1
  require 'kookaburra/api_driver'
2
+ require 'delegate'
4
3
  require 'active_support/json'
5
4
 
6
5
  class Kookaburra
7
- # Use this for your APIDriver base class if your application implements a JSON
8
- # webservice API. The JsonApiDriver abstracts away the details of translating
9
- # JSON <-> Ruby and setting the necessary request headers.
6
+ # Delegates all methods (by default) to and instance of
7
+ # {Kookaburra::APIDriver}.
10
8
  #
11
- # @example Create a widget via API
12
- # module MyApp
13
- # module Kookaburra
14
- # class APIDriver < ::Kookaburra::JsonApiDriver
15
- # def create_widget(data)
16
- # post '/widgets', data
17
- # end
18
- # end
19
- # end
20
- # end
21
- #
22
- # @note This implementation uses `ActiveSupport::JSON` to handle JSON
23
- # translation.
24
- class JsonApiDriver < APIDriver
25
- # @private
26
- J = ActiveSupport::JSON
27
-
28
- # @param [Kookaburra::RackDriver] app_driver
29
- def initialize(app_driver)
30
- @app_driver = app_driver
9
+ # Expects the application's API to accept and respond with JSON formatted
10
+ # data. All methods will decode the response body using
11
+ # `ActiveSupport::JSON.decode`. Methods that take input data ({#post} and
12
+ # {#put}) will encode the post data using `ActiveSupport::JSON.encode`.
13
+ class JsonApiDriver < SimpleDelegator
14
+ #
15
+ # Sets both the "Content-Type" and "Accept" headers to "application/json".
16
+ #
17
+ # @option options [Kookaburra::APIDriver] :api_driver (Kookaburra::APIDriver.new)
18
+ # The APIDriver instance to be delegated to. Changing this is probably
19
+ # only useful for testing.
20
+ def initialize(options = {})
21
+ api_driver = options[:api_driver] || APIDriver.new(:app_host => options[:app_host])
22
+ api_driver.headers.merge!(
23
+ 'Content-Type' => 'application/json',
24
+ 'Accept' => 'application/json'
25
+ )
26
+ super(api_driver)
31
27
  end
32
28
 
33
- protected
29
+ def post(path, data, *args)
30
+ request(:post, path, data, *args)
31
+ end
34
32
 
35
- # Sets headers for HTTP Basic authentication if your application requires
36
- # it.
37
- #
38
- # @example
39
- # def create_widget(data)
40
- # authorize('api_user', 'api_password')
41
- # post '/widgets', data
42
- # end
43
- #
44
- # @param [String] username
45
- # @param [String] password
46
- def authorize(username, password)
47
- @app_driver.authorize(username, password)
33
+ def put(path, data, *args)
34
+ request(:put, path, data, *args)
48
35
  end
49
36
 
50
- # Make a JSON post request to the server
51
- #
52
- # @param [String] path The path portion of the URI that should be requested
53
- # @param [Hash] data The data that should be translated into a JSON post
54
- # body for the request.
55
- #
56
- # @return [Hash] The decoded response from the server
57
- #
58
- # @example
59
- # # in your JsonApiDriver subclass
60
- # def create_widget(data)
61
- # post '/widgets', data
62
- # end
63
- #
64
- # # in a method within your GivenDriver
65
- # api.create_widget(:name => 'Foo')
66
- # #=> {:id => 1, :name => 'Foo', :description => ''}
67
- def post(path, data)
68
- do_json_request(:post, path, data)
37
+ def get(path, *args)
38
+ request(:get, path, nil, *args)
69
39
  end
70
40
 
71
- def put(path, data)
72
- do_json_request(:put, path, data)
41
+ def delete(path, *args)
42
+ request(:delete, path, nil, *args)
73
43
  end
74
44
 
75
- def get(path, data)
76
- do_json_request(:get, path, data)
45
+ private
46
+
47
+ def request(type, path, data = nil, *args)
48
+ # don't want to send data to methods that don't accept it
49
+ args = [path, encode(data), args].flatten.compact
50
+ output = __getobj__.send(type, *args)
51
+
52
+ decode(output)
77
53
  end
78
54
 
79
- private
55
+ def encode(data)
56
+ ActiveSupport::JSON.encode(data) unless data.nil?
57
+ end
80
58
 
81
- def do_json_request(method, path, data)
82
- json_request_headers = {
83
- 'Content-Type' => 'application/json',
84
- 'Accept' => 'application/json'
85
- }
86
- response = @app_driver.send(method.to_sym, path, J.encode(data), json_request_headers)
87
- J.decode(response)
59
+ def decode(data)
60
+ ActiveSupport::JSON.decode(data)
88
61
  end
89
62
  end
90
63
  end
@@ -1,37 +1,43 @@
1
1
  require 'delegate'
2
2
 
3
3
  class Kookaburra
4
- # Each instance of {Kookaburra} has its own instance of TestData. This object
4
+ # Each instance of {Kookaburra} has its own instance of MentalModel. This object
5
5
  # is used to maintain a shared understanding of the application state between
6
6
  # your {GivenDriver} and your {UIDriver}. You can access the various test data
7
7
  # collections in your test implementations via {Kookaburra#get_data}.
8
- class TestData
8
+ #
9
+ # The mental model is not intended to represent a copy of all of the data
10
+ # within your application. Rather it is meant to represent the mental image of
11
+ # the data that a user of your application might have while working with your
12
+ # system. Certainly you *can* store whatever you want in it, but thinking
13
+ # about it in these terms can help you design better, more robust tests.
14
+ class MentalModel
9
15
  def initialize
10
16
  @data = {}
11
17
  end
12
18
 
13
- # TestData instances will respond to any message that has an arity of 0 by
14
- # returning either a new or existing {TestData::Collection} having the name
19
+ # MentalModel instances will respond to any message that has an arity of 0 by
20
+ # returning either a new or existing {MentalModel::Collection} having the name
15
21
  # of the method.
16
22
  def method_missing(name, *args)
17
23
  return super unless args.empty?
18
24
  @data[name] ||= Collection.new(name)
19
25
  end
20
26
 
21
- # TestData instances respond to everything.
27
+ # MentalModel instances respond to everything.
22
28
  #
23
29
  # @see #method_missing
24
30
  def respond_to?
25
31
  true
26
32
  end
27
33
 
28
- # A TestData::Collection behaves much like a `Hash` object, with the
34
+ # A MentalModel::Collection behaves much like a `Hash` object, with the
29
35
  # exception that it will raise an {UnknownKeyError} rather than return nil
30
36
  # if you attempt to access a key that has not been set. The exception
31
37
  # attempts to provide a more helpful error message.
32
38
  #
33
39
  # @example
34
- # widgets = Kookaburra::TestData::Collection.new('widgets')
40
+ # widgets = Kookaburra::MentalModel::Collection.new('widgets')
35
41
  #
36
42
  # widgets[:foo] = :a_foo
37
43
  #
@@ -39,14 +45,14 @@ class Kookaburra
39
45
  # #=> :a_foo
40
46
  #
41
47
  # # Raises an UnknownKeyError
42
- # test_data.widgets[:bar]
48
+ # mental_model.widgets[:bar]
43
49
  class Collection < SimpleDelegator
44
50
  # @param [String] name The name of the collection. Used to provide
45
51
  # helpful error messages when unknown keys are accessed.
46
52
  def initialize(name)
47
53
  @name = name
48
54
  data = Hash.new do |hash, key|
49
- raise UnknownKeyError, "Can't find test_data.#{@name}[#{key.inspect}]. Did you forget to set it?"
55
+ raise UnknownKeyError, "Can't find mental_model.#{@name}[#{key.inspect}]. Did you forget to set it?"
50
56
  end
51
57
  super(data)
52
58
  end