Testing Rails: Model vs Integration
-
Upload
david-librera -
Category
Software
-
view
433 -
download
0
Transcript of Testing Rails: Model vs Integration
Testing? Why not!
David Librera ([email protected])
Cantiere Creativo
24/02/2016
|| (Cantiere Creativo) RSpec 24/02/2016 1 / 43
Quando scriviamo un’applicazione Rails, ci troviamo spesso in un ecosistema moltocomplesso e strutturato di directories e files
Models (ActiveRecord::Base)Controllers (ActionController::Base)Views (ActionView::Base)Presenters (Showcase::Presenter)Queries (Admino::Query::Base)Jobs ( ActiveJob::Base )Inputs ( SimpleForm::Inputs )...
|| (Cantiere Creativo) RSpec 24/02/2016 2 / 43
Vediamo velocemente a cosa servono i vari tipi dioggetti
|| (Cantiere Creativo) RSpec 24/02/2016 3 / 43
Models
I model hanno la responsabilità di gestite la persistenza dei dati a livello di DB
app/models/user.rb
class User < ActiveRecordhas_one :account
def last_loginend
end
|| (Cantiere Creativo) RSpec 24/02/2016 4 / 43
Controllers
I controller eseguono un’azione a livello di server e restituiscono una risposta al client
app/controllers/home_controller.rb
class HomeController < ApplicationControllerdef index
render template: ’home/index’end
end
|| (Cantiere Creativo) RSpec 24/02/2016 5 / 43
Views
Le views sono la definizione di come l’utente deve visualizzare la risposta fornita
app/views/home/index.html.erb
<h2>Wonderful world </h2><p>Sono le <%= Time.now.to_s %></p>
|| (Cantiere Creativo) RSpec 24/02/2016 6 / 43
Presenters
I presenter sono dei SimpleDelegator che rendono ’presentabili’ all’utente i dati di un oggetto
app/presenters/user_presenter.rb
class UserPresenter < Showcase :: Presenterdef full_name
[object.first_name , object.last_name ]. compact.join(‘ ‘)end
end
app/views/users/show.html.erb
<%- user = present(User.first)<span>Nome completo : <%= user.full_name %></span>
|| (Cantiere Creativo) RSpec 24/02/2016 7 / 43
Queries
I query object sono classi il cui compito è quello di definire delle query complesse sui model
app/queries/users_query.rb
class UsersQuery < Admino ::Query ::Basestarting_scope { Users.all }
def self.complex_query(attr)chain = User.all...chain
endend
|| (Cantiere Creativo) RSpec 24/02/2016 8 / 43
Jobs
I jobs sono operazioni complesse che effettuamo macro operazioni sui dati
app/jobs/remove_user_account.rb
class RemoveUserAccount < ActiveJob ::Basedef perform(args)
u = User.find(args[: user_id ])u.account = nilUserMailer.dropped_account_mailer(u). deliver_now
endend
somewhere_in_code
...RemoveUserAccount.new(user.id). perform_now...
|| (Cantiere Creativo) RSpec 24/02/2016 9 / 43
Supponiamo adesso di voler testare quanto più possibile ilCRUD di una risorsa abbastanza complessa.
Prenderemo in esame un modello che rappresenta unimmobile in vendita.
|| (Cantiere Creativo) RSpec 24/02/2016 10 / 43
Immobile
create_table "estate_proposals", force: :cascade do |t|t.integer "operator_id"t.integer "client_id"t.integer "zone_id"t.integer "signaler_id"t.integer "operation"t.integer "building_type_id"t.integer "rooms"t.integer "bedrooms"t.string "address"t.text "notes"t.integer "building_status"t.string "estimated_restructuring"t.integer "warm_up"t.integer "floor"t.integer "total_floors"t.integer "spending_condominium"t.integer "condominiums_number"t.integer "surface"t.boolean "garden", default: falset.integer "garden_surface"t.boolean "garage", default: false
end
|| (Cantiere Creativo) RSpec 24/02/2016 11 / 43
E mi sono trattenuto!!!!!!
|| (Cantiere Creativo) RSpec 24/02/2016 12 / 43
Possiamo anche pensare di spezzare la tabella inpiù tabelle e usare una direttiva has_one
:submodel per alleggerire la tabella.
Ciò non toglie che abbiamo molti campi da testare!
|| (Cantiere Creativo) RSpec 24/02/2016 13 / 43
Possiamo anche pensare di spezzare la tabella inpiù tabelle e usare una direttiva has_one
:submodel per alleggerire la tabella.
Ciò non toglie che abbiamo molti campi da testare!
|| (Cantiere Creativo) RSpec 24/02/2016 13 / 43
Se non troviamo un buon modo di organizzare i test, il doveraggiungere campi ad un model può diventare un processo lungo,
noioso e frustrante.
Nella peggiore delle ipotesi (che poi è quella che si verifica più spesso)il programmatore smette semplicemente di scrivere i test, soprattutto
durante la fase di release del progetto
|| (Cantiere Creativo) RSpec 24/02/2016 14 / 43
Se non troviamo un buon modo di organizzare i test, il doveraggiungere campi ad un model può diventare un processo lungo,
noioso e frustrante.
Nella peggiore delle ipotesi (che poi è quella che si verifica più spesso)il programmatore smette semplicemente di scrivere i test, soprattutto
durante la fase di release del progetto
|| (Cantiere Creativo) RSpec 24/02/2016 14 / 43
Facciamo un esempio pratico di quanto detto
|| (Cantiere Creativo) RSpec 24/02/2016 15 / 43
Partiamo dai feature test
|| (Cantiere Creativo) RSpec 24/02/2016 16 / 43
C’è una corrente di pensiero per cui i soli test diintegrazione possono bastare!
|| (Cantiere Creativo) RSpec 24/02/2016 17 / 43
Ma possiamo sempre fare i soli test di integrazione?
Pensando al model di prima, proviamo a testare il form dicreazione
Che scenari possiamo immaginare?
|| (Cantiere Creativo) RSpec 24/02/2016 18 / 43
Ma possiamo sempre fare i soli test di integrazione?
Pensando al model di prima, proviamo a testare il form dicreazione
Che scenari possiamo immaginare?
|| (Cantiere Creativo) RSpec 24/02/2016 18 / 43
Ma possiamo sempre fare i soli test di integrazione?
Pensando al model di prima, proviamo a testare il form dicreazione
Che scenari possiamo immaginare?
|| (Cantiere Creativo) RSpec 24/02/2016 18 / 43
spec/features/manage_estate_proposals_spec.rb
require ’rails_helper ’
RSpec.feature ‘Managing estate proposals ‘, type: :feature dodescribe ‘Creating an entry ‘ do
given (: new_page) { Pages :: EstateProposals ::New.new }describe ‘field xxx ‘ do
scenario ‘with a valid value ‘ donew_page.loadnew_page.xxx_field.set ‘valid ‘
new_page.submit!
expect(new_page ).to have_noticeend
scenario ‘with an invalid value ‘ donew_page.loadnew_page.xxx_field.set ‘invalid ‘
new_page.submit!
expect(new_page ).to have_alertend
endend
end
|| (Cantiere Creativo) RSpec 24/02/2016 19 / 43
Ecco, se ora volessimo, con le sole features, testare i varifield del nostro model, scriveremo qualcosa lungo come laDivina Commedia, portandoci dietro tutti i problemi di
manutenzione dei file lunghi.
|| (Cantiere Creativo) RSpec 24/02/2016 20 / 43
|| (Cantiere Creativo) RSpec 24/02/2016 21 / 43
Se ci fermiano un attimo a pensare, il nostro controllerdovrebbe avere, nella stragrande maggioranza dei casi,
questo aspetto
app/controllers/estate_proposals_controller.rb
class EstateProposalsController < ApplicationControllerdef create
@resource = EstateProposal.create(permitted_params)respond_with @resource
endend
|| (Cantiere Creativo) RSpec 24/02/2016 22 / 43
Il controller può solo completare con successo ofallire, quindi il test di integrazione dovrebbe
considerare solo questi 2 scenari
|| (Cantiere Creativo) RSpec 24/02/2016 23 / 43
O bene bene, o male male.
Non ci interessa sapere come si comporta (a livello utente)ogni singolo campo.
Usiamo strumenti come SimpleForm, che sappiamo(speriamo!) segnalano correttamente i campi errati, quindi a
noi basta solo che la risposta sia Verde o Rossa
|| (Cantiere Creativo) RSpec 24/02/2016 24 / 43
O bene bene, o male male.
Non ci interessa sapere come si comporta (a livello utente)ogni singolo campo.
Usiamo strumenti come SimpleForm, che sappiamo(speriamo!) segnalano correttamente i campi errati, quindi a
noi basta solo che la risposta sia Verde o Rossa
|| (Cantiere Creativo) RSpec 24/02/2016 24 / 43
spec/features/manage_estate_proposals_spec.rb
require ’rails_helper ’
RSpec.feature ‘Managing estate proposals ‘, type: :feature dodescribe ‘Creating an entry ‘ do
given (: new_page) { Pages :: EstateProposal ::New.new }
scenario ‘with valid values ‘ donew_page.loadnew_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_noticeend
scenario ‘with invalid values ‘ donew_page.loadnew_page.submit!
expect(new_page ).to have_alertend
endend
|| (Cantiere Creativo) RSpec 24/02/2016 25 / 43
Adesso deleghiamo ai test sui model, appoggiandoci alibrerie come soulda-matcher, il compito di testare la validità
di ogni singolo campo
|| (Cantiere Creativo) RSpec 24/02/2016 26 / 43
spec/models/estate_proposal_spec.rb
require ’rails_helper ’
RSpec.describe EstateProposal , type: :model doit { is_expected.to allow_value(’xxx’).for(:field) }it { is_expected.to validate_presence_of (: another_field) }...
end
|| (Cantiere Creativo) RSpec 24/02/2016 27 / 43
Con questa scomposizione, con molta probabilità lemodifiche al controller, e relativamente al test di
integrazione, non saranno più necessarie.
Eventuali modifiche alla logica avranno luogo quasiesclusivamente sul model, che è un file abbastanza snello da
leggere, non facendo più scappare il programmatore.
|| (Cantiere Creativo) RSpec 24/02/2016 28 / 43
Con questa scomposizione, con molta probabilità lemodifiche al controller, e relativamente al test di
integrazione, non saranno più necessarie.
Eventuali modifiche alla logica avranno luogo quasiesclusivamente sul model, che è un file abbastanza snello da
leggere, non facendo più scappare il programmatore.
|| (Cantiere Creativo) RSpec 24/02/2016 28 / 43
Proviamo adesso ad astrarre la logica dei test di integrazionein modo da semplificare anche la scrittura delle features
|| (Cantiere Creativo) RSpec 24/02/2016 29 / 43
spec/support/shared_examples/resource_you_can_create.rb
RSpec.shared_example ‘a resource you can create ‘ dodescribe ‘Creating an entry ‘ do
scenario ‘with valid values ‘ donew_page.loadfields.each do |field , value|
new_page.send("#{field.to_s}_field").set valueendnew_page.submit!
expect(new_page ).to have_noticeend
scenario ‘with invalid values ‘ donew_page.loadnew_page.submit!
expect(new_page ).to have_alertend
endend
|| (Cantiere Creativo) RSpec 24/02/2016 30 / 43
Adesso l’aspetto del nostro test sarà questo
spec/features/manage_estate_proposals_spec.rb
require ’rails_helper ’
RSpec.feature ‘Managing estate proposals ‘, type: :feature doit_behaves_like ‘a resource you can create ‘
given (: new_page) { Pages :: EstateProposal ::New.new }given (: fields) do
{field1: ’value1 ’,field2: ’value2 ’,field3: ’value3 ’
}end
endend
|| (Cantiere Creativo) RSpec 24/02/2016 31 / 43
Altro aspetto dei CRUD: i filtri sulle pagine index!
Vediamo un caso d’uso
|| (Cantiere Creativo) RSpec 24/02/2016 32 / 43
Altro aspetto dei CRUD: i filtri sulle pagine index!
Vediamo un caso d’uso
|| (Cantiere Creativo) RSpec 24/02/2016 32 / 43
|| (Cantiere Creativo) RSpec 24/02/2016 33 / 43
Come per il form di creazione, noi voglimoassicurarci che tutti quei campi funzionino.
Ma è compito del test di integrazione?
|| (Cantiere Creativo) RSpec 24/02/2016 34 / 43
Come per il form di creazione, noi voglimoassicurarci che tutti quei campi funzionino.
Ma è compito del test di integrazione?
|| (Cantiere Creativo) RSpec 24/02/2016 34 / 43
NOOOO!!
|| (Cantiere Creativo) RSpec 24/02/2016 35 / 43
Come prima, facendo una considerazione,dobbiamo cercare di capire quali sono le azioni
basiliari e i loro esiti.
|| (Cantiere Creativo) RSpec 24/02/2016 36 / 43
In questo caso noi possiamoFare una ricerca senza inserire alcun campo ottenendo tutti irecordsFare una ricerca inserendo TUTTI i campi ottenendo degli specificirecordsFare una ricerca inserendo TUTTI i campi senza ottenere records
Ovviamente stiamo dando per scontato che la concatenazione delleclausole where funzioni, ma stiamo usando Rails, no?
|| (Cantiere Creativo) RSpec 24/02/2016 37 / 43
spec/features/as_user/manage_estate_proposals_spec.rb
...describe ‘When I filter for estate_proposals ‘ do
given (: index_page) { Pages:: EstateProposals ::Index.new }given (: record) { create (: estate_proposal , :with_all_fields) }
before { index_page.load }
scenario ‘withour any search field I find the record ‘ doexpect(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
|| (Cantiere Creativo) RSpec 24/02/2016 38 / 43
app/controllers/estate_proposals_controller
class EstateController < ApplicationControllerdef index
@query = EstateProposalsQuery.new(params)@collection = @query.scoperespond_with @collection
endend
|| (Cantiere Creativo) RSpec 24/02/2016 39 / 43
Come prima, non è necessario scendere in maggioredettaglio
Il compito di assicurarsi che, inseriti i vari campi, ilrecord venga trovato, è del model.
|| (Cantiere Creativo) RSpec 24/02/2016 40 / 43
spec/queries/estate_proposals_query_spec.rb
describe ".title_matches" dolet(: with_a_valid_title) do
create (: estate_proposal , title: ‘A title ‘)endlet(: result) { EstateProposalsQuery.new(query: query ). scope }let(:query) { { title_matches: title } }
context ‘with a matching value ‘ dolet(:title) { "title" }it ‘retrieves the record ‘ do
expect(result ).to match_array [with_a_valid_title]end
end
context ‘without a matching value ‘ dolet(:title) { "foobar" }it ‘does not retrieve the record ‘ do
expect(result ).to match_array [with_a_valid_title]end
end
|| (Cantiere Creativo) RSpec 24/02/2016 41 / 43
app/models/estate_proposal.rb
class EstateProposal < ActiveRecord ::Basescope :title_matches , ->(text) do
where( at[:title]. matches("%#{ text}%") ) }end
def self.atself.arel_table
endend
|| (Cantiere Creativo) RSpec 24/02/2016 42 / 43
|| (Cantiere Creativo) RSpec 24/02/2016 43 / 43