ephemeral_response 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.markdown ADDED
@@ -0,0 +1,18 @@
1
+ History
2
+ =======
3
+
4
+ 0.2.0 / master
5
+ --------------
6
+
7
+ #### Enhancements
8
+
9
+ * Fixtures now have use .yml extension instead of .fixture.
10
+ * Varying POST data and query strings create new fixtures. Previously, GET /
11
+ and GET /?foo=bar resulted in the same fixture.
12
+ * Ability to reset configuration with EphemeralResponse::Configuration.reset
13
+ * Ability to white list certain hosts. Responses will not be saved for Requests
14
+ made to hosts in the white list.
15
+ Use EphemeralResponse::Configuration.white_list = "localhost"
16
+ * Ephemeral response prints to the Net/HTTP debugger when establishing a
17
+ connection. Just set http.set_debug_output = $stdout to see when Ephemeral
18
+ Response connects to a host.
data/README.markdown CHANGED
@@ -3,50 +3,87 @@ Ephemeral Response
3
3
 
4
4
  _Save HTTP responses to give your tests a hint of reality._
5
5
 
6
- This is pretty much NetRecorder without the fakeweb dependency.
7
-
8
6
  ## Premise
9
7
 
10
- 1. run tests
8
+ Web responses are volatile. Servers go down, API's change, responses change and
9
+ everytime something changes, your tests should fail. Mocking out web responses
10
+ may speed up your test suite but the tests essentially become lies. Ephemeral
11
+ Response encourages you to run your tests against real web services while
12
+ keeping your test suite snappy by caching the responses and reusing them until
13
+ they expire.
14
+
15
+ 1. run test suite
11
16
  2. all responses are saved to fixtures
12
- 3. run tests
13
- 4. Return the cached response if it exists and isn't out of date.
17
+ 3. disconnect from the network
18
+ 4. run test suite
19
+
20
+ ## Example
21
+
22
+ require 'benchmark'
23
+ require 'ephemeral_response'
14
24
 
15
- If a cached response exists but is out of date, update it with the real response
25
+ EphemeralResponse.activate
16
26
 
17
- Cache the response if it doesn't exist
27
+ 5.times do
28
+ puts Benchmark.realtime { Net::HTTP.get "example.com", "/" }
29
+ end
18
30
 
19
- ## Usage
31
+ 1.44242906570435 # First request caches the response as a fixture
32
+ 0.000689029693603516
33
+ 0.000646829605102539
34
+ 0.00064396858215332
35
+ 0.000645875930786133
20
36
 
21
- `$ vi spec/spec_helper.rb`
37
+ ## With Rspec
22
38
 
23
39
  require 'ephemeral_response'
24
40
 
25
41
  Spec::Runner.configure do |config|
42
+
26
43
  config.before(:suite) do
27
44
  EphemeralResponse.activate
28
45
  end
46
+
29
47
  config.after(:suite) do
30
48
  EphemeralResponse.deactivate
31
49
  end
50
+
32
51
  end
33
52
 
53
+ All responses are cached in yaml files within spec/fixtures/ephemeral\_response.
54
+
55
+ I'd recommend git ignoring this directory to ensure your tests always hit the
56
+ remote service at least once and to prevent credentials (like API keys) from
57
+ being stored in your repo.
58
+
34
59
  ### Configuration
35
- You can change the fixture directory which defaults to "spec/fixtures/ephemeral_response"
60
+
61
+ Change the fixture directory; defaults to "spec/fixtures/ephemeral\_response"
36
62
 
37
63
  EphemeralResponse::Configuration.fixture_directory = "test/fixtures/ephemeral_response"
38
64
 
39
- You can change the elapsed time for when a fixture will expire; defaults to 24 hours
65
+ Change the elapsed time for when a fixture will expire; defaults to 24 hours
40
66
 
41
67
  EphemeralResponse::Configuration.expiration = 86400 # 24 hours in seconds
42
68
 
43
- You can also pass a block when setting expiration which gets instance_eval'd
44
- giving you access to the awesome helper method `one_day`
69
+ Pass a block when setting expiration to gain access to the awesome helper
70
+ method `one_day`
45
71
 
46
- EphemeralResponse::Configuration.expiration do
47
- one_day * 30 # 60 * 60 * 24 * 30
72
+ EphemeralResponse::Configuration.expiration = lambda do
73
+ one_day * 30 # Expire in thirty days: 60 * 60 * 24 * 30
48
74
  end
49
75
 
76
+ Always allow requests to be made to a host by adding it to the white list.
77
+ Helpful when running ephemeral response with selenium which makes requests to
78
+ the local server.
79
+
80
+ EphemeralResponse::Configuration.white_list = "localhost", "smackaho.st"
81
+
82
+ ## Similar Projects
83
+ * [Net Recorder](http://github.com/chrisyoung/netrecorder)
84
+ * [Stalefish](http://github.com/jsmestad/stale_fish)
85
+ * [VCR](http://github.com/myronmarston/vcr)
86
+
50
87
  ## Note on Patches/Pull Requests
51
88
 
52
89
  * Fork the project.
@@ -59,4 +96,4 @@ giving you access to the awesome helper method `one_day`
59
96
 
60
97
  ## Copyright
61
98
 
62
- Copyright (c) 2010 Sandro Turriate. See LICENSE for details.
99
+ Copyright (c) 2010 Sandro Turriate. See MIT_LICENSE for details.
data/Rakefile CHANGED
@@ -14,8 +14,9 @@ begin
14
14
  gem.homepage = "http://github.com/sandro/ephemeral_response"
15
15
  gem.authors = ["Sandro Turriate"]
16
16
  gem.add_development_dependency "rspec", ">= 1.2.9"
17
- gem.add_development_dependency "yard", ">= 0"
17
+ gem.add_development_dependency "yard", ">= 0.5.0"
18
18
  gem.add_development_dependency "fakefs", ">= 0.2.1"
19
+ gem.add_development_dependency "unicorn", ">= 1.0.0"
19
20
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
21
  end
21
22
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.unshift("lib")
2
+ require 'rubygems'
3
+ require 'lib/ephemeral_response'
4
+ require 'benchmark'
5
+
6
+ EphemeralResponse::Configuration.expiration = 5
7
+ EphemeralResponse.activate
8
+
9
+ # Run benchmarks against thefuckingweather.com
10
+ # The first request takes much longer than the rest
11
+ def benchmark_request(number=1)
12
+ uri = URI.parse('http://thefuckingweather.com/?RANDLOC=')
13
+ time = Benchmark.realtime do
14
+ Net::HTTP.get(uri)
15
+ end
16
+ puts "Request #{number} took #{time} secs"
17
+ end
18
+
19
+ 5.times {|n| benchmark_request n + 1 }
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift("lib")
2
+ require 'rubygems'
3
+ require 'lib/ephemeral_response'
4
+
5
+ # Don't create fixtures for the localhost domain
6
+ EphemeralResponse::Configuration.white_list = 'localhost'
7
+
8
+ EphemeralResponse::Configuration.expiration = 5
9
+ EphemeralResponse.activate
10
+
11
+ # Start an HTTP server on port 19876 using netcat
12
+ process = IO.popen %(echo "HTTP/1.1 200 OK\n\n" | nc -l 19876)
13
+ at_exit { Process.kill :KILL, process.pid }
14
+ sleep 1
15
+
16
+ # Make a request to the server started above
17
+ # No new fixtures are created in spec/fixtures/ephemeral_response/
18
+ uri = URI.parse('http://localhost:19876/')
19
+ Net::HTTP.get(uri)
20
+
21
+ # Fixtures are still created for Google
22
+ uri = URI.parse('http://www.google.com/')
23
+ Net::HTTP.get(uri)
24
+
25
+ puts "The following directory should only contain a fixture for google"
26
+ puts
27
+ listing_cmd = %(ls #{File.expand_path(EphemeralResponse::Configuration.fixture_directory)})
28
+ puts listing_cmd
29
+ puts %x(#{listing_cmd})
@@ -19,6 +19,20 @@ module EphemeralResponse
19
19
  @expiration || one_day
20
20
  end
21
21
 
22
+ def reset
23
+ @expiration = nil
24
+ @fixture_directory = nil
25
+ @white_list = nil
26
+ end
27
+
28
+ def white_list
29
+ @white_list ||= []
30
+ end
31
+
32
+ def white_list=(*hosts)
33
+ @white_list = hosts.flatten
34
+ end
35
+
22
36
  protected
23
37
 
24
38
  def one_day
@@ -1,7 +1,7 @@
1
1
  module EphemeralResponse
2
2
  class Fixture
3
3
  attr_accessor :response
4
- attr_reader :method, :uri, :created_at
4
+ attr_reader :request, :uri, :created_at
5
5
 
6
6
  def self.fixtures
7
7
  @fixtures ||= {}
@@ -11,20 +11,27 @@ module EphemeralResponse
11
11
  @fixtures = {}
12
12
  end
13
13
 
14
- def self.find(uri, method)
15
- fixtures[Fixture.new(uri, method).identifier]
14
+ def self.find(uri, request)
15
+ fixtures[Fixture.new(uri, request).identifier]
16
16
  end
17
17
 
18
18
  def self.load_all
19
19
  clear
20
20
  if File.directory?(Configuration.fixture_directory)
21
- Dir.glob("#{Configuration.fixture_directory}/*.fixture", &method(:load_fixture))
21
+ Dir.glob("#{Configuration.fixture_directory}/*.yml", &method(:load_fixture))
22
22
  end
23
23
  fixtures
24
24
  end
25
25
 
26
26
  def self.load_fixture(file_name)
27
- fixture = YAML.load_file file_name
27
+ register YAML.load_file(file_name)
28
+ end
29
+
30
+ def self.find_or_initialize(uri, request, &block)
31
+ find(uri, request) || new(uri, request, &block)
32
+ end
33
+
34
+ def self.register(fixture)
28
35
  if fixture.expired?
29
36
  FileUtils.rm fixture.path
30
37
  else
@@ -32,26 +39,22 @@ module EphemeralResponse
32
39
  end
33
40
  end
34
41
 
35
- def self.respond_to(uri, method)
36
- fixture = Fixture.new(uri, method)
37
- unless fixtures[fixture.identifier]
42
+ def self.respond_to(uri, request)
43
+ find_or_initialize(uri, request) do |fixture|
38
44
  fixture.response = yield
39
- fixture.save
40
- fixtures[fixture.identifier] = fixture
41
- end
42
- fixtures[fixture.identifier].response
45
+ fixture.register
46
+ end.response
43
47
  end
44
48
 
45
- def initialize(uri, method)
46
- @method = method
47
- @uri = uri
48
- @uri.normalize!
49
+ def initialize(uri, request)
50
+ @uri = uri.normalize
51
+ @request = deep_dup request
49
52
  @created_at = Time.now
50
53
  yield self if block_given?
51
54
  end
52
55
 
53
56
  def ==(other)
54
- %w(method uri created_at response).all? do |attribute|
57
+ %w(request_identifier uri_identifier created_at response).all? do |attribute|
55
58
  send(attribute) == other.send(attribute)
56
59
  end
57
60
  end
@@ -65,12 +68,19 @@ module EphemeralResponse
65
68
  end
66
69
 
67
70
  def identifier
68
- Digest::SHA1.hexdigest(normalized_name)[0..6]
71
+ Digest::SHA1.hexdigest("#{uri_identifier}#{request_identifier}")
72
+ end
73
+
74
+ def method
75
+ request.method
69
76
  end
70
77
 
71
78
  def normalized_name
72
- normalized_path = uri.path.gsub(/\/$/, '').gsub('/', '-')
73
- [uri.host, method, normalized_path].join("_")
79
+ [uri.host, method, fs_path].compact.join("_").gsub(/[\/.]/, '-')
80
+ end
81
+
82
+ def fs_path
83
+ uri.path.dup.sub!(/^\/(.+)$/, '\1')
74
84
  end
75
85
 
76
86
  def path
@@ -78,7 +88,14 @@ module EphemeralResponse
78
88
  end
79
89
 
80
90
  def register
81
- FakeWeb.register_uri(method, uri, :response => response)
91
+ unless Configuration.white_list.include? uri.host
92
+ save
93
+ self.class.register self
94
+ end
95
+ end
96
+
97
+ def request_identifier
98
+ request.to_yaml.split(//).sort
82
99
  end
83
100
 
84
101
  def save
@@ -88,10 +105,24 @@ module EphemeralResponse
88
105
  end
89
106
  end
90
107
 
108
+ def uri_identifier
109
+ if uri.query
110
+ parts = uri.to_s.split("?", 2)
111
+ parts[1] = parts[1].split('&').sort
112
+ parts
113
+ else
114
+ uri.to_s
115
+ end
116
+ end
117
+
91
118
  protected
92
119
 
120
+ def deep_dup(object)
121
+ Marshal.load(Marshal.dump(object))
122
+ end
123
+
93
124
  def generate_file_name
94
- "#{normalized_name}_#{identifier}.fixture"
125
+ "#{normalized_name}_#{identifier[0..6]}.yml"
95
126
  end
96
127
  end
97
128
  end
@@ -3,18 +3,28 @@ module Net
3
3
  alias request_without_ephemeral_response request
4
4
  alias connect_without_ephemeral_response connect
5
5
 
6
+ attr_reader :uri
7
+
6
8
  def connect
7
9
  end
8
10
  private :connect
9
11
 
12
+ def do_start_without_ephemeral_response
13
+ D "EphemeralResponse: establishing connection to #{uri}"
14
+ connect_without_ephemeral_response
15
+ @started = true
16
+ end
17
+ private :do_start_without_ephemeral_response
18
+
10
19
  def generate_uri(request)
11
20
  scheme = use_ssl? ? "https" : "http"
12
- URI.parse("#{scheme}://#{conn_address}:#{conn_port}#{request.path}")
21
+ @uri = URI.parse("#{scheme}://#{conn_address}:#{conn_port}#{request.path}")
13
22
  end
14
23
 
15
24
  def request(request, body = nil, &block)
16
- EphemeralResponse::Fixture.respond_to(generate_uri(request), request.method) do
17
- connect_without_ephemeral_response
25
+ generate_uri(request)
26
+ EphemeralResponse::Fixture.respond_to(uri, request) do
27
+ do_start_without_ephemeral_response
18
28
  request_without_ephemeral_response(request, body, &block)
19
29
  end
20
30
  end
@@ -7,7 +7,7 @@ require 'ephemeral_response/configuration'
7
7
  require 'ephemeral_response/fixture'
8
8
 
9
9
  module EphemeralResponse
10
- VERSION = "0.1.0".freeze
10
+ VERSION = "0.2.0".freeze
11
11
 
12
12
  def self.activate
13
13
  deactivate
@@ -18,6 +18,7 @@ module EphemeralResponse
18
18
  def self.deactivate
19
19
  Net::HTTP.class_eval do
20
20
  remove_method(:generate_uri) if method_defined?(:generate_uri)
21
+ remove_method(:uri) if method_defined?(:uri)
21
22
  alias_method(:connect, :connect_without_ephemeral_response) if private_method_defined?(:connect_without_ephemeral_response)
22
23
  alias_method(:request, :request_without_ephemeral_response) if method_defined?(:request_without_ephemeral_response)
23
24
  end
@@ -2,6 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe EphemeralResponse::Configuration do
4
4
  subject { EphemeralResponse::Configuration }
5
+
5
6
  describe "#fixture_directory" do
6
7
  it "has a default" do
7
8
  subject.fixture_directory.should == "spec/fixtures/ephemeral_response"
@@ -36,4 +37,51 @@ describe EphemeralResponse::Configuration do
36
37
  end
37
38
  end
38
39
  end
40
+
41
+ describe "#reset" do
42
+ it "resets expiration, fixture directory, and white list to the defaults" do
43
+ subject.fixture_directory = "test/fixtures/ephemeral_response"
44
+ subject.expiration = 1
45
+ subject.white_list = 'localhost'
46
+ subject.fixture_directory.should == "test/fixtures/ephemeral_response"
47
+ subject.expiration.should == 1
48
+ subject.white_list.should == ['localhost']
49
+
50
+ subject.reset
51
+
52
+ subject.fixture_directory.should == "spec/fixtures/ephemeral_response"
53
+ subject.expiration.should == 86400
54
+ subject.white_list.should == []
55
+ end
56
+
57
+ it "resets white list after the default has been modified" do
58
+ subject.white_list << "localhost"
59
+ subject.reset
60
+ subject.white_list.should be_empty
61
+ end
62
+ end
63
+
64
+ describe "#white_list" do
65
+ it "defaults to an empty array" do
66
+ subject.white_list.should == []
67
+ end
68
+
69
+ it "allows hosts to be pushed onto the white list" do
70
+ subject.white_list << 'localhost'
71
+ subject.white_list << 'smackaho.st'
72
+ subject.white_list.should == %w(localhost smackaho.st)
73
+ end
74
+ end
75
+
76
+ describe "#white_list=" do
77
+ it "sets a single host" do
78
+ subject.white_list = 'localhost'
79
+ subject.white_list.should == ['localhost']
80
+ end
81
+
82
+ it "sets multiple hosts" do
83
+ subject.white_list = 'localhost', 'smackaho.st'
84
+ subject.white_list.should == ['localhost', 'smackaho.st']
85
+ end
86
+ end
39
87
  end