riot-gear 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ .DS_Store
2
+ .bundle/*
3
+ *.tmproj
4
+ pkg/*
5
+ .rvmrc
6
+ *.swp
7
+ .watchr
8
+ .yardoc
9
+ Gemfile.lock
10
+
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ -r README.markdown
2
+ --protected
3
+ --no-private
4
+ -
5
+ CHANGELOG
6
+ MIT-LICENSE
data/CHANGELOG ADDED
@@ -0,0 +1,39 @@
1
+ # @markup markdown
2
+
3
+ # 0.0.9
4
+
5
+ * get, post, put, delete can have responses saved for use in other requests within the same (or nested) context [jaknowlden]
6
+
7
+ ```ruby
8
+ context GoNuts do
9
+ post(:the_new_nut) do
10
+ { :path => "/make/some", :body => "..." }
11
+ end
12
+
13
+ get do
14
+ { :path => "/get/some/#{response(:the_new_nut)["id"]}" }
15
+ end
16
+ end
17
+ ```
18
+
19
+ The last get, post, put, or delete in the context will still be the one used for assertions.
20
+
21
+ * get, post, put, delete can take a block that expects a settings hash to be returned (with a path) [jaknowlden]
22
+
23
+ ```ruby
24
+ context GoNuts do
25
+ post do
26
+ { :path => "/make/some", :body => "..." }
27
+ end
28
+
29
+ get do
30
+ { :path => "/get/some", :query => { ... } }
31
+ end
32
+ end
33
+ ```
34
+
35
+ * tons of documentation [jaknowlden]
36
+
37
+ # 0.0.8 and before
38
+
39
+ See the commit log: http://github.com/thumblemonks/riot-gear/commits/master
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group "test" do
6
+ gem "rake", ">=0.8.7"
7
+ gem "webmock", ">=1.6.1"
8
+ end
9
+
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Justin Knowlden, Thumble Monks
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,17 +1,90 @@
1
1
  # Riot Gear
2
2
 
3
- Real HTTP-based smoke testing with a real testing framework; [Riot](http://thumblemonks.github.com/riot) + [HTTParty]().
3
+ Riot Gear is a framework for HTTP-based smoke testing using a real testing framework; [Riot](http://thumblemonks.github.com/riot) + [HTTParty](http://github.com/jnunemaker/httparty). The principle impetus for creating Riot Gear came from a desire to easily develop a suite of smoke tests for a few, JSON-based web services. On one hand you could use Riot Gear to develop in-depth integration tests hitting a local testing environment that are constantly run through your continuous integration system. On the other hand, you could use Riot Gear to implement real smoke tests that hit your production environment frequently and/or maybe after a release. From this you can derive that Riot Gear does not intend to replace Selenium (Se) or any of Se's "competitors". Nope ... Riot Gear just wants to make the real-world testing of web-enabled APIs easier.
4
4
 
5
- require 'teststrap'
5
+ How Riot Gear does this is by combining two things I enjoy; Riot for its lightweight, contextual, and flexible testing framework; and HTTParty for its simplistic and powerful approach to providing HTTP enabled APIs (so to speak). The resulting DSL essentially allows one to mix HTTParty behavior directly with Riot behavior; i.e. build up and make an HTTP request and then test its response.
6
6
 
7
- context "Logging into Example.com as foo" do
7
+ Here's an example involving a hypothetical login and login failure:
8
+
9
+ require 'riot/gear'
10
+
11
+ context "Logging into Example.com as good.user" do
12
+ base_uri "http://example.com"
13
+ post "/session", :body => {"email" => "good.user@example.com", "password" => "p4$sw0R)"}
14
+
15
+ asserts_status.equals(200)
16
+ asserts_header("Content-Type").equals("application/json;charset=utf-8")
17
+ asserts_json("user.name").equals("Slick Rick")
18
+ end
19
+
20
+ context "Failing to log into Example.com as good.user" do
21
+ base_uri "http://example.com"
22
+ post "/session", :body => {"email" => "good.user@example.com", "password" => "y0uF41l"}
23
+
24
+ asserts_status.equals(403)
25
+ asserts_header("Content-Type").equals("application/json;charset=utf-8")
26
+ asserts_json("error.message").matches(/email or password is invalid/i)
27
+ end
28
+
29
+ If you're familiar with Riot and/or HTTParty you'll recognize their natures immediately. To begin, there are two Riot `context` blocks. Each context (and any of the sub-contexts) are independent from each other; this is in keeping with the nature of Riot. Within each context there will be setup code and then validation code. Setup code will most likely be reflected as HTTParty calls - though you should feel free to use Riot's helpers, setups, and hookups; whereas validation code will always be Riot (until this statement is wrong).
30
+
31
+ Thus, in the two example contexts, the setup code involves telling HTTParty what the `base_uri` is that any HTTP requests for that context will be made to; followed by the actual HTTP request - a `post` to `/session` with login credentials. After the `post` is sent, the `HTTParty::Response` object is made available for validation as the Riot helper named `response`.
32
+
33
+ Riot Gear provides a few built-in assertions for validating common response information, which you see above: `asserts_status`, `asserts_header`, and `asserts_json`. Each of these generate normal Riot assertions that can have assertion macros applied to them (eg. `equals`, `kind_of`, `matches`, `nil`, `exists`, etc.). You could easily replace what `asserts_status` does in the example above with this:
34
+
35
+ asserts("status code") do
36
+ response.code
37
+ end.equals(200)
38
+
39
+ ## Priming a request
40
+
41
+ A common problem when testing services is that you need to perform a few activities before you can perform the one you want to test. For instance, you may need to login and create some resource before you can test an update to that resource. This is simple enough in Riot Gear since the last request made through `get`, `post`, `put`, `delete`, or `head` is the one whose response will be validated. For instance:
42
+
43
+ require 'riot/gear'
44
+
45
+ context "Updating a playlist" do
8
46
  base_uri "http://example.com"
9
- get "/session/new?username=foo&password=password"
47
+
48
+ post "/session", :body => {"email" => "good.user@example.com", "password" => "p4$sw0R)"}
49
+ persist_cookie("example_session")
50
+
51
+ post "/user/playlists", :body => {"name" => "Dubsteppin to the oldies"}
52
+ put "/user/playlists/dubsteppin-to-the-oldies", :body => {"name" => "Dubsteppin to the newbies"}
10
53
 
11
54
  asserts_status.equals(200)
12
55
  asserts_header("Content-Type").equals("application/json;charset=utf-8")
56
+ asserts_json("data.message").equals("Playlist updated successfully")
57
+ end
58
+
59
+ The `post` commands and the `put` will execute in the order they are defined. However, only the response from the `put` will be used for validation. It also would not matter where in the context you put the assertions because they always run last. For instance, this context is effectually the same as the one above:
60
+
61
+ require 'riot/gear'
62
+
63
+ context "Updating a playlist" do
64
+ base_uri "http://example.com"
65
+
66
+ post "/session", :body => {"email" => "good.user@example.com", "password" => "p4$sw0R)"}
67
+ persist_cookie("example_session")
68
+
69
+ asserts_json("data.message").equals("Playlist updated successfully")
13
70
 
14
- # ... more stuff
71
+ post "/user/playlists", :body => {"name" => "Dubsteppin to the oldies"}
72
+
73
+ asserts_header("Content-Type").equals("application/json;charset=utf-8")
74
+
75
+ put "/user/playlists/dubsteppin-to-the-oldies", :body => {"name" => "Dubsteppin to the newbies"}
76
+
77
+ asserts_status.equals(200)
15
78
  end
16
79
 
17
- Lots lots more to come soon.
80
+ *This fact is likely to change soon.*
81
+
82
+ ## Coming soon
83
+
84
+ * Reusable macro-like blocks
85
+ * Priming a test against another host via `at_host`
86
+ * Sequential command processing
87
+
88
+ ## License
89
+
90
+ Riot Gear is released under the MIT license. See {file:MIT-LICENSE}.
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'rubygems'
2
-
3
- require 'rake'
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
4
  require 'rake/testtask'
5
5
 
6
6
  #
@@ -15,34 +15,14 @@ task(:console) { exec "irb -r boot" }
15
15
 
16
16
  #
17
17
  # Testing
18
+ task :default => ["test"]
18
19
 
19
20
  Rake::TestTask.new("test") do |t|
20
21
  t.libs << "test"
21
22
  t.pattern = "test/**/*_test.rb"
23
+ #t.warning = true
22
24
  t.verbose = false
23
25
  end
24
- Rake::Task["test"].instance_variable_set(:@full_comment, nil) # Dumb dumb dumb
25
- Rake::Task["test"].comment = "Run the tests!"
26
26
 
27
27
  task :default => :test
28
28
 
29
- #
30
- # Some monks like diamonds. I like gems.
31
-
32
- begin
33
- require 'jeweler'
34
- Jeweler::Tasks.new do |gem|
35
- gem.name = "riot-gear"
36
- gem.summary = "Riot + HTTParty smoke testing framework"
37
- gem.description = "Riot + HTTParty smoke testing framework. You'd use it for integration testing with real HTTP requests and responses"
38
- gem.email = "gus@gusg.us"
39
- gem.homepage = "http://github.com/thumblemonks/riot-gear"
40
- gem.authors = ["Justin 'Gus' Knowlden"]
41
- gem.add_dependency 'riot'
42
- gem.add_dependency 'httparty'
43
- gem.add_development_dependency 'webmock', ">=1.6.1"
44
- end
45
- Jeweler::GemcutterTasks.new
46
- rescue LoadError
47
- puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
48
- end
@@ -4,6 +4,12 @@ module Riot
4
4
  module Gear
5
5
  module AssertsHeader
6
6
 
7
+ # Generates an assertion that retrieves the value of some header from the last response.
8
+ #
9
+ # asserts_header("Content-Length").equals("125")
10
+ #
11
+ # @param [String] header_key the name of the header
12
+ # @return [Riot::Assertion] an assertion block that macros can be applied to
7
13
  def asserts_header(header_key)
8
14
  asserts("header variable #{header_key}") { response.headers[header_key] }
9
15
  end
@@ -4,15 +4,29 @@ module Riot
4
4
  module Gear
5
5
  module AssertsJson
6
6
 
7
- # Returns the value of passing the JSON path to the json_path helper. If a handler block is provided,
8
- # that block will be called with the value and the response from the block will be used as the actual
9
- # in the assertion test.
7
+ # Generates an assertion that based on the value returned from passing the JSON path to the json_path
8
+ # helper. If a handler block is provided, that block will be called with the value and the response
9
+ # from the block will be used as the actual in the assertion test.
10
10
  #
11
- # asserts_json("http.status_code").equals(200)
11
+ # context "testing a hash" do
12
+ # setup do
13
+ # {"a" => {"b" => {"c" => {"d" => "foo"}}}}
14
+ # end
12
15
  #
13
- # asserts_json("http") do |value|
14
- # value["status_code"]
15
- # end.equals(200)
16
+ # asserts_json("a.b.c.d").equals("foo")
17
+ # asserts_json("a['b'].c['d']").equals("foo")
18
+ #
19
+ # asserts_json("a.b") do |value|
20
+ # value["c"]
21
+ # end.equals({"d" => "foo"})
22
+ # end
23
+ #
24
+ # This is useful for testing actual JSON responses from some service that are converted to a hash by
25
+ # HTTParty.
26
+ #
27
+ # @param [String] json_string a JSON looking path
28
+ # @param [lambda] &handler an optional block for filtering the actual value
29
+ # @return [Riot::Assertion] an assertion block that macros can be applied to
16
30
  def asserts_json(json_string, &handler)
17
31
  asserts("value from body as json:#{json_string}") do
18
32
  value = json_path(response, json_string)
@@ -4,6 +4,12 @@ module Riot
4
4
  module Gear
5
5
  module AssertsStatus
6
6
 
7
+ # Generates an assertion that retrieves the status code from the last response. The statuc code will
8
+ # be an integer.
9
+ #
10
+ # asserts_status.equals(200)
11
+ #
12
+ # @return [Riot::Assertion] an assertion block that macros can be applied to
7
13
  def asserts_status
8
14
  asserts("status code") { response.code }
9
15
  end
@@ -5,6 +5,23 @@ module Riot
5
5
  module Gear
6
6
  module PersistCookie
7
7
 
8
+ # Graft a cookie (name and value) from the last response onto the next and subsequent requests. Only
9
+ # applies to multiple requests within a {Riot::Context}. For instance, if you log into your service,
10
+ # you will probably want to pass along whatever the session cookie was to the next request in the test.
11
+ #
12
+ # context "Get new messages" do
13
+ # base_uri "http://example.com"
14
+ # post "/session?email=foo@bar.baz&password=beepboopbop"
15
+ #
16
+ # persist_cookie("example_session")
17
+ #
18
+ # get "/user/messages.json"
19
+ # asserts_status.equals(200)
20
+ # asserts_json("messages").length(8)
21
+ # end
22
+ #
23
+ # @param [String] cookie_name the name of a cookie to persist
24
+ # @return [Riot::Assertion] an assertion block that macros can be applied to
8
25
  def persist_cookie(cookie_name)
9
26
  hookup do
10
27
  if cookie_value = cookie_values[cookie_name]
@@ -2,9 +2,18 @@ require 'httparty'
2
2
 
3
3
  module Riot
4
4
  module Gear
5
+ # Here we prepare a {Riot::Context} to have HTTParty bound to it. Basically, this means that you can
6
+ # use HTTParty within a context the same way you would inside any class or you would normally use it in.
7
+ # Anything you can do with HTTParty, you can do within a context ... and then you can test it :)
8
+ #
9
+ # Great pains are made to ensure that the HTTParty setup bound to one context does not interfere setup
10
+ # bound to another context.
5
11
  class RiotPartyMiddleware < ::Riot::ContextMiddleware
6
12
  register
7
13
 
14
+ # Prepares the context for HTTParty support.
15
+ #
16
+ # @param [Riot::Context] context the context to prepare
8
17
  def call(context)
9
18
  setup_faux_class(context)
10
19
  setup_helpers(context)
@@ -16,49 +25,93 @@ module Riot
16
25
  private
17
26
 
18
27
  # Only cluttering anonymous classes with HTTParty stuff. Keeps each context safe from collision ... in
19
- # theory
28
+ # theory.
29
+ #
30
+ # @param [Riot::Context] context the context to create the setup for
31
+ # @todo Fix this so that settings like +base_uri+ can be inherited
20
32
  def setup_faux_class(context)
21
33
  context.setup(true) do
34
+ @saved_responses = {}
35
+
22
36
  Class.new do
23
37
  include HTTParty
24
38
  # debug_output $stderr
25
39
  end
26
40
  end
27
41
 
28
- context.helper(:response) { @smoke_response }
42
+ context.helper(:response) do |name=nil|
43
+ @saved_responses[name] || @smoke_response
44
+ end
29
45
  end # setup_faux_class
30
46
 
47
+ # Returns the list of methods that do something; like make a network call.
31
48
  #
32
- # Method proxying. This is the meat of the DSL.
33
-
49
+ # @return [Array<Symbol>]
34
50
  def actionable_methods; [:get, :post, :put, :delete, :head]; end
35
51
 
36
- def proxy_action_methods(context)
37
- proxy_class_methods_to_context(context, actionable_methods) do |situation, result|
38
- situation.instance_eval { @smoke_response = result }
39
- end
40
- end
41
-
52
+ # Returns the list of methods that configure actionable HTTParty methods. The {HTTParty.options} and
53
+ # {HTTParty.default_options} methods are explicitly excluded from this list
54
+ #
55
+ # @return [Array<Symbol>]
42
56
  def proxiable_methods
43
57
  methods = HTTParty::ClassMethods.instance_methods.map { |m| m.to_s.to_sym }
44
58
  methods - actionable_methods - [:options, :default_options]
45
59
  end
46
60
 
47
- def proxy_httparty_hookups(context)
48
- proxy_class_methods_to_context(context, proxiable_methods)
49
- end
50
-
51
- # Basically, we're just passing standard HTTParty setup methods onto situation via hookups
52
- def proxy_class_methods_to_context(context, methods, &proxy_block)
53
- methods.each do |method_name|
54
- (class << context; self; end).__send__(:define_method, method_name) do |*args|
61
+ # Bind the set of actionable methods to a given context.
62
+ #
63
+ # Basically, we're just passing standard HTTParty setup methods onto situation via hookups. These
64
+ # hookups - so long as the topic hasn't changed yet - are bound to an anonymous class that has
65
+ # HTTParty included to it. Meaning, this is how you call get, post, put, delete from within a
66
+ # test.
67
+ #
68
+ # There are couple of different forms for these actions. As you would expect, there's:
69
+ #
70
+ # get "/path", :query => {...}, ...
71
+ #
72
+ # But you can also record the response for use later in the test:
73
+ #
74
+ # post(:new_thing) do
75
+ # { :path => "/things", :body => ... }
76
+ # end
77
+ #
78
+ # get do # this response will be used for assertions since it's last
79
+ # { :path => "/things/#{response(:new_thing).id}/settings" }
80
+ # end
81
+ #
82
+ # @param [Riot::Context] context the context to add the helper to
83
+ def proxy_action_methods(context)
84
+ context_eigen = (class << context; self; end)
85
+ actionable_methods.each do |method_name|
86
+ context_eigen.__send__(:define_method, method_name) do |*args, &settings_block|
55
87
  hookup do
56
- result = topic.__send__(method_name, *args)
57
- yield(self, result) if proxy_block
88
+ if settings_block
89
+ name = args.first
90
+ options = instance_eval(&settings_block)
91
+ path = options.delete(:path) || "/"
92
+ else
93
+ name = nil
94
+ path, options = *args
95
+ end
96
+ result = topic.__send__(method_name, path, options || {})
97
+ @saved_responses[name] = result
98
+ @smoke_response = result # TODO remove this after it's certain no usages in the wild
58
99
  end
59
- end # class << context
100
+ end
60
101
  end # methods.each
61
- end # proxy_class_methods_to_context
102
+ end
103
+
104
+ # Bind the set of proxiable (non-action) methods to a given context.
105
+ #
106
+ # @param [Riot::Context] context the context to add the helper to
107
+ def proxy_httparty_hookups(context)
108
+ context_eigen = (class << context; self; end)
109
+ proxiable_methods.each do |method_name|
110
+ context_eigen.__send__(:define_method, method_name) do |*args|
111
+ hookup { topic.__send__(method_name, *args) }
112
+ end
113
+ end # methods.each
114
+ end
62
115
 
63
116
  #
64
117
  # Helpful helpers
@@ -70,13 +123,13 @@ module Riot
70
123
 
71
124
  # Maps a JSON string to a Hash tree. For instance, give this hash:
72
125
  #
73
- # json_object = {"a" => {"b" => "c" => {"d" => "foo"}}}
126
+ # json_object = {"a" => {"b" => {"c" => {"d" => "foo"}}}}
74
127
  #
75
128
  # You could retrieve the value of 'd' via JSON notation in any of the following ways:
76
129
  #
77
130
  # json_path(json_object, "a.b.c.d")
78
131
  # => "foo"
79
- # json_path(json_object, "a['b'].c[d]")
132
+ # json_path(json_object, "a['b'].c['d']")
80
133
  # => "foo"
81
134
  #
82
135
  # You can even work with array indexes
@@ -84,6 +137,8 @@ module Riot
84
137
  # json_object = {"a" => {"b" => "c" => ["foo", {"d" => "bar"}]}}
85
138
  # json_path(json_object, "a[b].c[1].d")
86
139
  # => "bar"
140
+ #
141
+ # @param [Riot::Context] context the context to add the helper to
87
142
  def helper_json_path(context)
88
143
  context.helper(:json_path) do |dictionary, path|
89
144
  return nil if path.to_s.empty?
@@ -102,6 +157,8 @@ module Riot
102
157
  # "stupid_marketing_tricks" => {"value" => "personal-information", ...},
103
158
  # ...
104
159
  # }
160
+ #
161
+ # @param [Riot::Context] context the context to add the helper to
105
162
  def helper_cookie_value(context)
106
163
  context.helper(:cookie_values) do
107
164
  response.header["set-cookie"].split("\n").inject({}) do |jar, cookie_str|