SeaShanty

SeaShanty is a tiny library that records HTTP interactions and replays responses for requests it has seen before.

The primary purpose is to speed up your test suite regardless of which test framework used by not relying on the network.

Installation

Add this line to your application's Gemfile:

gem 'sea_shanty'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install sea_shanty

Usage

The following is a minimal example of how to use SeaShanty.

require "rubygems"
require "minitest"
require "faraday"
require "sea_shanty"
require "sea_shanty/faraday"

SeaShanty.configure do |config|
  config.storage_dir = "fixtures/sea_shanty"
end
SeaShanty.intercept(:faraday)

class TestSeaShanty < Minitest::Test
  def test_fetch
    response = Faraday.get("https://example.com")
    assert_equal(200, response.status)
  end
end

The first time the test above is run, the request is sent as normal, because SeaShanty hasn't seen it before, and the request and response is stored, before the response is returned. On subsequent runs of the test, no requests will be sent over the network. Instead the response is loaded from the storage directory and returned.

CAUTION!

To prevent storing sensitive information like Authorization headers or credentials in the request body, use the Configuration#request_body_filter and Configuration#request_headers_filter described in the Configuration section.

Interccept requests in an HTTP library

The following call intercepts requests made with Faraday.

SeaShanty.intercept :faraday

Any HTTP requests made before the call to SeaShanty.intercept will not be handled by SeaShanty.

Configuration

  • storage_dir
    • The storage directory for the requests and responses.
    • A string that can either be an absolute path or a path relative to Dir.pwd
  • bypass
    • Tells if SeaShanty should be bypassed
    • A boolean. If it is true, SeaShanty will not look up or store responses, but just pass the request to the HTTP library
    • This can be overwritten by setting the environment variable SEA_SHANTY_BYPASS
  • readonly
    • Tells if SeaShanty should allow the HTTP library to send the request over the network
    • A boolean. If it is true, no requests are sent over the network. Instead SeaShanty will raise a SeaShanty::UnknownRequest error, if the request has not been seen before
    • This can be overwritten by setting the environment variable SEA_SHANTY_READONLY
  • generic_responses
    • Tells SeaShanty to return generic responses whenever the request url matches one of the keys
    • A hash where the keys are Regexps and the values are strings with the path to a file with a stored response relative to the storage_dir
    • The request url is matched against the Regexps, and the first match will make SeaShanty return the response stored in the file with the path in the hash value
  • request_body_filter
    • Is applied to the request body before figuring out if the request has been seen before, and before the request and response is saved
    • A callable object with an arity of 1.
    • Is called with the raw request body
    • Must return the filtered body
  • request_headers_filter
    • Is applied to the request headers before the request and response is saved
    • A callable object with an arity of 2
    • Is called once with each header name and value
    • Must return the filtered value
  • log
    • Is the destination, where SeaShanty should log to
    • Must be either an object that responds to write or a path to a file to write log entries to

Creating your own interceptor

Any object responding to intercept! and remove can be registered as interceptors in SeaShanty by calling SeaShanty.register_interceptor(:identifier, interceptor).

The intercept! method must be able to take an instance of a SeaShanty::RequestStore as its sole argument.

Using the RequestStore

When intercepting a request from the HTTP library, a SeaShanty::Request should be instantiated like so:

request = SeaShanty::Request.new(method: "GET", url: "https://example.com", headers: {}, body: "")

The method should be a String or symbol, url should be a String or an URI, headers should be a hash, and body a String or nil.

This Request objejct is then passed to RequestStore#fetch together with a block that takes no parameters and returns a SeaShanty::Response.

If the response is not loaded from the request store, the block is instead called.

To build a Response object use the initializer:

SeaShanty::Response.new(status: 200, message: "OK", headers: {}, body: "", original_response: response_from_library)

The status should be an Integer, message a String or nil, headers should be a hash, body a String or nil. original_response is optional provided as a convenience.

When RequestStore#fetch returns the response, it is the interceptor's responsibility to convert the Response object into a format the library understands. If the block to fetch was called, and original_response is set, Response#was_stored? is true, and the response from the HTTP library is available in Response#original_response. Otherwise the interceptor can retrieve the data from the Response object using attributes similarly named as the named parameters in the initializer.

Registering an interceptor

The Faraday interceptor is registered like so in lib/sea_shanty/faraday.rb:

require "sea_shanty/faraday/interceptor"

SeaShanty.register_interceptor :faraday, SeaShanty::Faraday::Interceptor.new

Supported HTTP libraries

In the current version Faraday is the only library SeaShanty supports.

Alternatives

Future development

  • [ ] Add interceptors for more HTTP libraries
  • [ ] Add optional response body compression to reduce storage requirements
  • [ ] Make it possible to overwrite stored requests and responses
  • [ ] Document Request, Response and RequestStore
  • [ ] More? Create an issue or send a pull request, if you think something is missing 🎉

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install.

Before creating a pull request, please make sure rake test passes, and rake standard has no suggestions.

Releasing a new version

To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

The SeaShanty versioning scheme follows SemVer 2.0.0.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/rbgrouleff/sea_shanty. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the SeaShanty project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.