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/Gemfile +1 -1
- data/Gemfile.lock +2 -1
- data/README.markdown +114 -135
- data/VERSION +1 -1
- data/kookaburra.gemspec +8 -10
- data/lib/kookaburra.rb +22 -31
- data/lib/kookaburra/api_driver.rb +95 -11
- data/lib/kookaburra/given_driver.rb +32 -16
- data/lib/kookaburra/json_api_driver.rb +43 -70
- data/lib/kookaburra/{test_data.rb → mental_model.rb} +15 -9
- data/lib/kookaburra/null_browser.rb +4 -0
- data/lib/kookaburra/test_helpers.rb +2 -4
- data/lib/kookaburra/ui_driver.rb +15 -11
- data/lib/kookaburra/ui_driver/ui_component.rb +22 -5
- data/spec/integration/test_a_rack_application_spec.rb +171 -137
- data/spec/kookaburra/api_driver_spec.rb +126 -0
- data/spec/kookaburra/json_api_driver_spec.rb +85 -30
- data/spec/kookaburra/{test_data_spec.rb → mental_model_spec.rb} +6 -6
- data/spec/kookaburra/ui_driver_spec.rb +5 -3
- data/spec/kookaburra_spec.rb +9 -41
- metadata +9 -11
- data/lib/kookaburra/rack_driver.rb +0 -109
- data/lib/kookaburra/utils/active_record_shared_connection.rb +0 -14
- data/spec/kookaburra/rack_driver_spec.rb +0 -42
data/lib/kookaburra.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'kookaburra/exceptions'
|
2
|
-
require 'kookaburra/
|
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
|
36
|
+
# @option options [Capybara::Session] :browser The browser driver
|
40
37
|
# that Kookaburra will interact with to run the tests.
|
41
|
-
# @option options [
|
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
|
-
@
|
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
|
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(:
|
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(:
|
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
|
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
|
78
|
-
# within your test implementation
|
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::
|
87
|
+
# @return [Kookaburra::MentalModel::Collection]
|
87
88
|
def get_data(collection_name)
|
88
|
-
|
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, :
|
95
|
+
dependency_accessor :given_driver_class, :ui_driver_class
|
95
96
|
|
96
|
-
def
|
97
|
-
|
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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
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
|
-
# @
|
13
|
-
|
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
|
29
|
+
# # Store the resulting widget data in the MentalModel object, so that
|
25
30
|
# # it can be referenced in other operations.
|
26
|
-
#
|
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::
|
38
|
-
#
|
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
|
-
@
|
41
|
-
@
|
47
|
+
@initialization_options = options
|
48
|
+
@mental_model = options[:mental_model]
|
42
49
|
end
|
43
50
|
|
44
51
|
protected
|
45
52
|
|
46
|
-
#
|
47
|
-
# instance was created with.
|
53
|
+
# Used to access your APIDriver in your own GivenDriver implementation
|
48
54
|
#
|
49
|
-
# @
|
50
|
-
# @return [Kookaburra::
|
51
|
-
|
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
|
-
#
|
54
|
-
#
|
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::
|
58
|
-
dependency_accessor :
|
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
|
-
#
|
8
|
-
#
|
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
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
29
|
+
def post(path, data, *args)
|
30
|
+
request(:post, path, data, *args)
|
31
|
+
end
|
34
32
|
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
72
|
-
|
41
|
+
def delete(path, *args)
|
42
|
+
request(:delete, path, nil, *args)
|
73
43
|
end
|
74
44
|
|
75
|
-
|
76
|
-
|
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
|
-
|
55
|
+
def encode(data)
|
56
|
+
ActiveSupport::JSON.encode(data) unless data.nil?
|
57
|
+
end
|
80
58
|
|
81
|
-
def
|
82
|
-
|
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
|
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
|
-
|
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
|
-
#
|
14
|
-
# returning either a new or existing {
|
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
|
-
#
|
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
|
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::
|
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
|
-
#
|
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
|
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
|