Rodrigo Rosenfeld Rosas

Introducing RackToolkit: a fast server and DSL designed to test Rack apps

Wed, 27 Jul 2016 18:43:00 +0000

I started to experiment with writing big Ruby web applications as a set of smaller and fast Rack applications connected by a router using Roda's multi_run plugin.

Such design allows the application to boot super fast in the development environment (and in the production environment too unless you prefer to eager load your code in production). Here's how the design looks like (I've written about AutoReloader in another article):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# config.ru
if ENV['RACK_ENV'] == 'development'
  require 'auto_reloader'
  AutoReloader.activate reloadable_paths: [ 'apps', 'lib', 'models' ]
  run ->(env) do
    AutoReloader.reload! do
      ActiveSupport::Dependencies.clear # avoid some issues
      require_relative 'apps/main'
      Apps::Main.call env
    end
  end
else
  require_relative 'apps/main'
  run Apps::Main
end

# apps/main.rb
require 'roda'
module Apps
  class Main < Roda
    plugin :multi_run
    # other plugins and middlewares are added, such as :error_handler, :not_found, :environments
    # and a logger middleware. They take some space, so I'm skipping them.

    def self.register_app(path, &app_block)
      # if you want to eager load files in production you'd change this method a bit
      ->(env) do
        require_relative path
        app_block[].call env
      end
    end

    run 'sessions', register_app('session'){ Session }
    run 'admin', register_app('admin') { Admin }
    # other apps
  end
end

# apps/base.rb
require 'roda'
module Apps
  class Base < Roda
    # add common plugins for rendering, CSRF protection, middlewares
    # like ETag, authentication and so on. Most apps would inherit from this.
    route{|r| process r }
    private
    def process(r)
      protect_from_csrf # added by some CSRF plugin
    end
  end
end

# apps/admin.rb
require_relative 'base'
module Apps
  class Admin < Base
    private
    def process(r)
      super # protects from forgery and so on
      r.get('/'){ "TODO Admin interface" }
      # ...
    end
  end
end

Then I want to be able to test those applications separately and for some of them I would only get confidence if I tested against a real server since I would want them to handle with cookies or streaming and checking for some HTTP headers injected by the real server and so on. And I wanted to be able to write such tests that could run as quickly as possible.

I started experimenting with Puma and noticed it can start a new server really fast (like 1ms in my development environment). I didn't want to add many dependencies so I decided to create some simple DSL over 'net/http' stdlib since its API is not much friendly. The only dependencies so far are http-cookie and Puma (WEBrick does not support full hijack support and it doesn't provide a simple API to serve Rack apps either and it's much slower to boot). Handling cookies correctly to keep the user session is not trivial so I decided to introduce the http-cookie dependency to manage a cookie jar.

That's how rack_toolkit was born.

Usage

This way I can start the server before the test suite starts, change the Rack app served by the server dynamically, and stop it when the suite finishes (or you can simply start and stop it for each example since it boots really fast). Here's a spec_helper.rb you could use if you are using RSpec:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# spec/spec_helper.rb
require 'rack_toolkit'
RSpec.configure do |c|
  c.add_setting :server
  c.add_setting :skip_reset_before_example

  c.before(:suite) do
    c.server = RackToolkit::Server.new start: true
    c.skip_reset_before_example = false
  end

  c.after(:suite) do
    c.server.stop
  end

  c.before(:context){ @server = c.server }
  c.before(:example) do
    @server = c.server
    @server.reset_session! unless c.skip_reset_before_example
  end
end

Testing the Admin app should be easy now:

1
2
3
4
5
6
7
8
9
# spec/apps/admin_spec.rb
require_relative '../../apps/admin'
RSpec.describe Admin do
  before(:all){ @server.app = Admin }
  it 'shows an expected main page' do
    @server.get '/'
    expect(@server.last_response.body).to eq 'TODO Admin interface'
  end
end

Please take a look at the project's README for more examples and supported API. RackToolkit allows you to get the current_path, referer, manages cookies sessions, provides a DSL for get, post and post_data on top of 'net/http' from stdlib, allows overriding the environment variables sent to the Rack app, simulating an https request as if the app was behind some proxy like Nginx, supports "virtual hosts", default domain, performing requests to external Internet urls and many other options.

Future development

It currently doesn't provide a DSL for quickly access elements from the response body, filling in forms and submitting them, but I plan to work on this once I need it. It won't ever support JavaScript though unless it would be possible at some point to do so without slowing it down significantly. If you want to work on such DSL, please let me know.

Performance

The test suite currently runs 33 requests and finishes in ~50ms (skipping the external request example). It's that fast.

Feedback

Looking forward your suggestions to improve it. Your feedback is very welcomed.

comments powered byDisqus