kookaburra 0.18.3 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
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