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# config.ru
2if ENV['RACK_ENV'] == 'development'
3 require 'auto_reloader'
4 AutoReloader.activate reloadable_paths: [ 'apps', 'lib', 'models' ]
5 run ->(env) do
6 AutoReloader.reload! do
7 ActiveSupport::Dependencies.clear # avoid some issues
8 require_relative 'apps/main'
9 Apps::Main.call env
10 end
11 end
12else
13 require_relative 'apps/main'
14 run Apps::Main
15end
16
17# apps/main.rb
18require 'roda'
19module Apps
20 class Main < Roda
21 plugin :multi_run
22 # other plugins and middlewares are added, such as :error_handler, :not_found, :environments
23 # and a logger middleware. They take some space, so I'm skipping them.
24
25 def self.register_app(path, &app_block)
26 # if you want to eager load files in production you'd change this method a bit
27 ->(env) do
28 require_relative path
29 app_block[].call env
30 end
31 end
32
33 run 'sessions', register_app('session'){ Session }
34 run 'admin', register_app('admin') { Admin }
35 # other apps
36 end
37end
38
39# apps/base.rb
40require 'roda'
41module Apps
42 class Base < Roda
43 # add common plugins for rendering, CSRF protection, middlewares
44 # like ETag, authentication and so on. Most apps would inherit from this.
45 route{|r| process r }
46 private
47 def process(r)
48 protect_from_csrf # added by some CSRF plugin
49 end
50 end
51end
52
53# apps/admin.rb
54require_relative 'base'
55module Apps
56 class Admin < Base
57 private
58 def process(r)
59 super # protects from forgery and so on
60 r.get('/'){ "TODO Admin interface" }
61 # ...
62 end
63 end
64end

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# spec/spec_helper.rb
2require 'rack_toolkit'
3RSpec.configure do |c|
4 c.add_setting :server
5 c.add_setting :skip_reset_before_example
6
7 c.before(:suite) do
8 c.server = RackToolkit::Server.new start: true
9 c.skip_reset_before_example = false
10 end
11
12 c.after(:suite) do
13 c.server.stop
14 end
15
16 c.before(:context){ @server = c.server }
17 c.before(:example) do
18 @server = c.server
19 @server.reset_session! unless c.skip_reset_before_example
20 end
21end

Testing the Admin app should be easy now:

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

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.

Powered by Disqus