Underneath (almost) all Ruby web application lies the Rack architecture. With minimal interface, it is simple to write a middleware. Let's see how to test it.

A middleware is anything which responds to call method (duck-typing).

Aside: Lambdas and procs in Ruby can be executed with .call so they can be middlewares as well.

Our example middleware allows requests to pass through only if the required headers are present:

class BlockActions
  def initialize(app)
    @app = app
  end

  def call(env)
    unless env["HTTP_X_CLIENT"] == "mobile" && env["HTTP_X_MOBILE_USER_ID"]
      return [
        400, # status
        { "Content-Type" => "application/json" }, # headers
        ["Invalid Request"] # response body
      ]
    end
    @app.call(env)
  end
end

Now in order to test this, we have to write integration tests which is fine, but they are slow. Especially if you want to benchmark the middleware to see if it is not doing some nasty thing in some edgecase that bumps up your response times.

As described before, rack middleware is a simple class with call method. Let's test it in isolation:

We need 2 things: the middleware instance & env to be passed in. Middleware instance is simple class instance, but to create env, we will use MockRequest

Rack::MockRequest.env_for("/", method: :get)

Our middleware does not care about routes so it might as well be:

Rack::MockRequest.env_for

Putting it into specs:

describe BlockActions do
  # Mock env to pass in middleware
  let(:env) { Rack::MockRequest.env_for }

  # dummy app to validate success
  let(:app) { ->(env) { [200, {}, "success"] } }

  subject { BlockActions.new(app) }

  it "allows if client & user-id headers are present" do
    env["HTTP_X_CLIENT"] = "mobile"
    env["HTTP_X_MOBILE_USER_ID"] = 1
    status, _headers, _response = subject.call(env)
    expect(status).to eq(200)
  end

  it "does not allow if client is missing" do
    env["HTTP_X_MOBILE_USER_ID"] = 1
    status, _headers, _response = subject.call(env)
    expect(status).to eq(400)
  end

  it "does not allow if mobile-user-id is missing" do
    env["HTTP_X_CLIENT"] = "mobile"
    status, _headers, _response = subject.call(env)
    expect(status).to eq(400)
  end

  it "does not allow if client is not mobile" do
    env["HTTP_X_CLIENT"] = "jared"
    status, _headers, _response = subject.call(env)
    expect(status).to eq(400)
  end
end

Because these are unit tests they run pretty fast:

Finished in 0.03861 seconds (files took 0.09118 seconds to load)
4 examples, 0 failures

This approach allows us to test all cases & also benchmark the middleware.

All code from this blog can be found in this executable script.