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