Vai al contenuto principaleVai al footer
ruby
|
03 ottobre 18

Testing delle applicazioni Rack

È facile fare il testing delle request HTTP all'interno di un'applicazione Rails. Ma come possiamo farlo nel caso in cui stiamo scrivendo una gem in puro Ruby senza supporti esterni?

L'anarchia fuori Rails

Ho trascorso gli ultimi 2 mesi lavorando sul sistema [SPID di AgID] (https://www.agid.gov.it/it/piattaforme/spid) per permettere una sua integrazione all'interno di qualunque applicazione Rails.

Volendo però rendere SPID accessibile a tutti i programmatori Ruby, e non solo Rails, è stato deciso di scendere di livello e di pensare ad una sua versione totalmente indipendente da qualunque framework per poi costruirci sopra successivamente i relativi adapter Sinatra o Rails. La scelta si è spostata dunque sul buon caro Rack.

Fintanto che si è trattato di testare in unit alcune funzionalità specifiche dell'applicazione il testing è stato facile e comodo. Ma quando ho iniziato ad affrontare il test delle richieste su Rack ho avuto difficoltà a trovare un meccanismo che rendesse gli specs comodi da scrivere e chiari da leggere.

Mentre per rspec-rails l'impostazione dei requests specs è quasi uno standard, nel mondo rack ho trovato differenti approcci, ma erano tutti, a parer mio, incomprensibili. Quelli che più si avvicinavamo all'idea che avevo avevano tutti come dipendenza Sinatra.

Alla fine mettendo insieme un po' di informazioni sono riuscito a fare una quadratura del problema. Vediamolo insieme.

Un middleware di esempio

Supponiamo di dover testare un modulo rack che, se riconosce un determinato path della richiesta, risponde con alcune variabili dell'env.

class MyExampleRackMiddleware
  attr_reader :app

  def initialize(app)
    @app = app
  end

  def call(env)
    request = Rack::Request.new(env)

    if request.path == "/server-info"
      return [
        200,
        {},
        [ request.fetch_header("MY_HEADER") ]
      ]
    end

    app.call(env)
  end
end

Creazione dell'app di test

Il primo passo è definire all'interno del nostro spec una sorta di mini applicazione da usare come soggetto dei nostri test. Per far questo possiamo utilizzare Rack::Builder.

RSpec.describe MyExampleRackMiddleware do
  let(:app) do
    Rack::Builder.new do
      use MyExampleRackMiddleware
      run ->(_env) { [200, { "Content-Type" => "text/plain" }, ["OK"]] }
    end
  end

  ...
end

Rack::Builder altro non è che il meccanismo che sta alla base degli stack di rails, sinatra o qualunque altra applicazione compatibile con rack. Di conseguenza possiamo considerare l'oggetto app come un'applicazione rack a tutti gli effetti.

Richieste HTTP

Ora dobbiamo poter gestire le singole richieste HTTP. Questo è possibile grazie alla classe Rack::MockRequest.

let(:request) { Rack::MockRequest.new(app) }

Rack::MockRequest crea un piccolo wrapper intorno alla nostra applicazione app che vogliamo testare. In particolare ci fornisce dei comodi metodi per creare delle chiamate HTTP e ci restitusce un oggetto di tipo Rack::MockResponse che ci permette di interagire con la risposta in maniera semplice.

Nel dettaglio un oggetto di tipo Rack::MockRequest fornisce un metodo per ogni possibile metodo HTTP: .get, .post, .delete con un firma semplificata

request.get("/a-path", params: { id: 1 }, "key" => value, ...)

In automatico tutto ciò che non è params verrà inserito nell'env della richiesta, mentre params verrà inserito o nell'url o nel body (nel caso di utilizzo di .post, .put)

Nel caso di sopra il path effettivo della richiesta sarà /a-path?id=1 e potremmo utilizzare env["key"] per accedere a value.

La raccomandazione è quella di convertire sempre l'oggetto env in un Rack::Request

request = Rack::Request.new(env)

in quanto esonera chi scrive il rack dal dover conoscere nel dettaglio le variabili d'ambiente e permette di accedere in maniera semplificata ai .params o alla .session senza doversi preoccupare del come.

Nel caso del nostro middleware, un possibile esempio potrebbe essere questo

describe "GET /server-info" do
  let(:response) do
    request.get("/server-info", params: {})
  end
end

Il test finale

Messi insieme i vari pezzi, è possibile scrivere il test nel modo seguente:

RSpec.describe MyExampleRackMiddleware do
  let(:app) do
    Rack::Builder.new do
      use MyExampleRackMiddleware
      run ->(_env) { [200, { "Content-Type" => "text/plain", ["OK"] }] }
    end
  end

  let(:request) { Rack::MockRequest.new(app) }

  describe "GET /server-info" do
    let(:response) do
      request.get("/server-info", "MY_HEADER" => "my-header-value")
    end

    it "returns 'MY_HEADER' content in response body" do
      expect(response.body).to eq "my-header-value"
    end
  end

  describe "GET /another-path" do
    let(:response) do
      request.get("/another-path", "MY_HEADER" => "my-header-value")
    end

    it "pass the request to the next middleware" do
      expect(response.body).to eq "OK"
    end
  end
end