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
endCreazione 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
...
endRack::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
endIl 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