coach icon indicating copy to clipboard operation
coach copied to clipboard

No defined way to test composition of middleware

Open nickcampbell18 opened this issue 7 years ago • 3 comments

Problem

Let's say I have a large application, with many middleware. Some middleware are simply composition units of others, for example:

class Middleware::Authenticate
  uses Middleware::ExtractBearerToken
  uses Middleware::LookupBearerToken
  uses Middleware::LogBearerToken

  requires :bearer_token
  provides :user

  def call
    provide(user: bearer_token.user)
    next_middleware.call
  end
end

I think this is a use-case which isn't currently tested well:

  • We have boot-time checking of the dependency tree (so we check that ExtractBearerToken provides :bearer_token)
  • We have unit tests of each sub-middleware behaving in the correct way in isolation.
  • We have request specs testing the overall route (including interactions of all the other middleware).

But there doesn't seem to be a simple way of saying, in a test, "run this smaller middleware chain" (i.e. invoke each sub-middleware and then run Middleware::Authenticate#call). The behaviours of this unit might vary depending on inputs/outputs of each sub-middleware in a way which isn't best expressed in isolated unit tests nor in the high-level request specs (which can be slow).

I think that endorsing this kind of testing to uncover interaction edge-cases would be incredibly valuable.

Proposal

Add a high-level API, either to coach/rspec or in the README, for running a full middleware chain like this.

The usage would look something like:

describe Middleware::Authenticate do
  subject(:instance) { Coach::Chain.new(described_class) } # this is the interesting bit
  
  it { is_expected.to call_next_middleware }
  it { is_expected.to provide(user: ...) }

  context "when a bearer token isn't in the request" do
    it "bails out before calling the main function"
  end
end

nickcampbell18 avatar Apr 17 '18 16:04 nickcampbell18

I'm aware that we could use Coach::Handler to construct a full request handler object, but that seems like overkill in specs.

nickcampbell18 avatar Apr 17 '18 16:04 nickcampbell18

I'm assuming we'll need some way to vary the actual input to the call, would it maybe look like

subject(:instance) { Coach::Chain.new(described_class, request: request) }

Maybe?

tragiclifestories avatar Apr 18 '18 10:04 tragiclifestories

+1, this bit me HARD

dmagliola avatar Jul 27 '18 14:07 dmagliola