Smart integration tests con RSpec
Recentemente ho lavorato ad un progetto per un gestionale. Nulla di più standard. Un backend di amministrazione e un frontend pubblico.
Un model però aveva catturato la mia attenzione: la rappresentazione di un immobile.
La tabella del db contiene circa 60 attributi. Tanti, lo so. Si può in questi casi spezzare il model in più sottomodel:
class Immobile < ActiveRecord::Base
has_one :superficie
has_one :anagrafica_proprietario
...
end
Questa scomposizione magari alleggerisce il model, ma si porta dietro un po' di problematiche nel caso dei form... ma ne parleremo prossimamente.
In entrami i casi comunque, i 60 campi rimangono.
I test di integrazione
Questo progetto era relativamente semplice, quindi l'ho sfruttato come banco di prova per potenziare un po' l'utilizzo dei test di integrazione.
L'esperimento che volevo fare era quello di tentare un approccio che si incentrasse quasi esclusivamente sull'utilizzo dei test di integrazione che, con poche righe di test, permettessero di coprire una buona percentuale di codice business.
La prima cosa da fare è quella di pensare agli scenari da testare. Un primo approccio è stato il seguente (si utilizza, per il modulo Pagine della gemma SitePrism:
require 'rails_helper'
RSpec.feature `Managing immobili`, type: :feature do
describe `Creating an entry` do
given(:new_page) { Pagine::Immobili::New.new }
describe `field xxx` do
scenario `with a valid value` do
new_page.load
new_page.xxx_field.set `valid`
new_page.submit!
expect(new_page).to have_notice
end
scenario `with an invalid value` do
new_page.load
new_page.xxx_field.set `invalid`
new_page.submit!
expect(new_page).to have_alert
end
end
end
end
Ora, in un approccio puramente BDD, ogni riga di codice dovrebbe essere anticipata da un test che ne giustifichi l'esistenza.
Se volessimo perseguire questo schema usando i soli test di integrazione, dovremmo immaginare di replicare questo codice per ogni campo ( o almeno, per ogni campo che necessita di una qualche validazione )

E la cosa potrebbe succedere presto! E quando succede questo, il codice dei test potrebbe divenrare noioso da mantenere, con il rischio che si smetta proprio di scrivere i tests.
Si può fare di meglio?
Pensiamo un secondo a come è fatta l'action del controller, che è, all'interno del nostro codice, il punto che rappresenta le medesime azioni che un utente può effetturare nella nostra applicazione:
class ImmobiliController < ApplicationController
def create
@resource = Immobile.create(permitted_params)
respond_to @resource
end
end
Se ci soffermiamo sul controller, ci rendiamo conto che le uniche 2 cose che possono succedere sono che la creazione avvenga con successo o che fallisca.
Aggiungiamo che, utilizziando gemme come SimpleForm e Responders, si può dare per scontato che i flash vengano correttamente settati i funzione dell'esito dell'azione.
Di conseguenza, possiamo immaginare solo 2 scenari:
require 'rails_helper'
RSpec.feature `Managing immobili`, type: :feature do
describe `Creating an entry` do
given(:new_page) { Pagine::Immobili::New.new }
scenario `with valid values` do
new_page.load
new_page.field1_field.set `valid`
new_page.field2_field.set `valid`
...
new_page.fieldn_field.set `valid`
new_page.submit!
expect(new_page).to have_notice
end
scenario `with invalid values` do
new_page.load
new_page.submit!
expect(new_page).to have_alert
end
end
end
Nel primo caso settiamo tutti i campi per verificare che il record venga creato. Nel secondo non ne settiamo nessuno per verificare che il record non venga creato.
Arrivati a questo punto, spostiamo nei test dei model tutta la logica di validazione dei campi, facendo utilizzo, ad esempio, di Shoulda Matchers
require 'spec_helper'
RSpec.describe Immobile, type: :model do
it { is_expected.to validate_presence_of(:mandatory_field) }
it { is_expected.to allow_value("a particular value").for(:another_field }
end
Con un'impostazione del genere, difficilmente ci sarà da mettere nuovamente mano alle features. Inoltre la validazione di un nuovo campo è ristretta una (o pochissime) righe di codice, rendendo così più semplice mantenere il codice.
I filtri di ricerca
Ogni index di una data risorsa, dovrebbe contenere un form che permetta di filtrare i suoi record.
Anche in questo caso, l'approccio di creare uno scenario per ogni filtro diventa pesante.
Gli scenari, alla fin fine, sono di solito 3:
- Senza passare alcun parametro, voglio vedere tutti i record (solitamente una index si comporta così di default)
- Passando tutti i parametri, voglio ottenere un particolare record
- Passando tutti i parametri, non voglio ottenere alcun record
describe `When I filter Immobili` do
given(:index_page) { Pages::Immobili::Index.new }
given(:record) { create(:immobile, :with_all_fields) }
before { index_page.load }
scenario `withour any search field I find the record` do
expect(index_page).to have_record(record)
end
scenario `with existing values I find the record` do
# here i fill the form
expect(index_page).to have_record(record)
end
scenario `with not existing values I do not find any record` do
#here I fill the form
expect(index_page).to_not have_record(record)
end
end
Il controller relativo assumerà un aspetto simile a questo:
class ImmobiliController < ApplicationController
def index
@query = ImmobiliQuery.new(params)
@collection = @query.scope
respond_with @collection
end
end
Adesso, sulla falsa riga di quanto fatto con la creazione di un immobile, deleghiamo al query object tutti i metodi scope della nostra risorsa
Si può fare, quindi, tutto in integrazione?
La risposta a questo punto sarebbe si, ma richiederebbe uno sforzo notevole, rendendo il codice noioso da scrivere.
Devo dire però che ho dovuto trascorrere molte e molte ore a scrivere, cancellare, riscrivere il codice, per trovare una soluzione che rendesse quasi divertente il testing.
Se non ci affacciamo mai al mondo di RSpec (o del testing in generale), passeremo molto tempo a pensare a come strutturare il nostro singolo test, continuando a preferire la verifica manuale tramite browser.