Vai al contenuto principaleVai al footer
Code
|
10 aprile 14

Unit e Integration test: ecco le gemme che usiamo per scrivere applicazioni solide

Esistono svariate tipologie di test, ma prenderemo in considerazione soltanto i test d’integrazione (o funzionali), volti a verificare che il software faccia effettivamente ciò che deve, e i test di unità, utilizzati per verificare che ogni unità di codice funzioni correttamente.

Lorenzo MasiniDeveloper

A seguito dell'articolo precedente vorrei proseguire sul tema del testing, questa volta descrivendo i tool e le metodologie che utilizziamo a Cantiere Creativo.

Test d'integrazione

I test d'integrazione sono dei test "ad alto livello", nei quali "prendiamo le vesti" dell'utente che effettivamente sta utilizzando l'applicazione e cerca di ottenere un risultato in risposta a una certa azione.

Questo genere di test verifica non solo il corretto comportamento di ogni singolo oggetto, ma anche le relazioni con gli altri componenti dell'applicazione.

Nella pratica dello sviluppo web un test d'integrazione — attraverso gemme quali Capybara — sfrutta un vero browser per compiere, sulle pagine, un determinato numero di azioni in maniera programmatica (come per esempio la submission di un form), per poi verificare uno specifico output visivo al termine del test. Seguendo l'esempio della submission del form, si aspetterà un messaggio di successo.

Sebbene il test d'integrazione sia senza dubbio il più completo e affidabile, dato che durante la sua esecuzione percorre l'intero stack risulta essere anche il più lento, soprattutto se la funzionalità da testare richiede l'interazione con JavaScript: siamo nell'ordine di 3-5 secondi a test.

In applicativi di medie dimensioni si arriva in breve tempo a collezionare centinaia di test, dunque una suite totalmente in integration è da sconsigliare per i suoi tempi di esecuzione: siamo nell'ordine delle decine di minuti!

La linea guida che seguiamo a Cantiere è quella di partire definendo l'happy path attraverso un test d'integrazione, per poi specificare il comportamento dell'applicativo, in ogni caso di fallimento, attraverso unit test sul layer specifico, con l'obiettivo di ridurre i tempi di esecuzione di un ordine di grandezza.

Unit testing

Un test di unità è specifico su una singola unità di codice e riproducendone le condizioni necessarie, verifica il suo comportamento nel caso di errore o fuori dal path di uso normale.

La distizione, chiaramente, non è cosí netta: ci sono alcune componenti dell'applicazione in cui è conveniente utlizzare i test di unità anche per l'happy path, come per esempio i Command e Query objects che affronterò in un prossimo post.

I test di unità sono anche particolarmente utili in fase di design di una certa componente software, proprio grazie alla loro specificità: plasmare una certa classe, o una certa interfaccia tramite TDD con test d'unità, aiuta a osservare il principio d'incapsulamento ed evitare il coupling, massimizzando quindi la riusabilità del codice.

Questo genere di test va considerato "usa e getta", almeno per quanto riguarda l'happy path, che per quanto detto prima, verrà probabilmente coperto da test di più alto livello.

Strumenti

Arriviamo finalmente alla pratica, parlando delle gemme che ci aiutano a concretizzare i concetti di cui abbiamo parlato in questo articolo.

SitePrism

SitePrism implementa il concetto di Page Object Pattern.

The Page Object pattern represents the screens of your web app as a series of objects

Un Page Object è un oggetto che si interpone tra i nostri test e la pagina reale. Questo layer di astrazione aggiuntivo ci permette di incapsulare in metodi le interazioni con la pagina reale e rendere i nostri test più indipendenti dalla sua struttura. Il risultato è quello di migliorare la DRYness della nostra suite di test e di conseguenza aumentarne la mantenibilità.

Consideriamo per esempio il caso in cui un utente voglia poter effettuare il login nella nostra applicazione.

Senza Page Objects dovremmo scrivere:

    feature 'As an user, in order to see my dashboard' do
      let(:user) { create(:user, email: email, password: password) }
      let(:email) { 'user@example.com' }
      let(:password) { 'foobar' }

      before do
        user # create user
      end

      scenario 'I want to sign in' do
        visit '/sessions/new'
        within("#session") do
          fill_in 'Login', :with => email
          fill_in 'Password', :with => password
        end
        click_button 'Sign in'
        expect(page).to have_content t('user.signin.success')
      end
    end

Che con SitePrism diventa:

    class SignInPage < Page
      set_url '/session/new'

      element :email, "input[name$='[email]']"
      element :password, "input[name$='[password]']"
      element :submit_button, "input[type='submit']"

      def sign_in_as(user, password)
        email_field.set user.email
        password_field.set password
        submit_button.click
      end
    end

    feature 'As an user, in order to see my dashboard' do
      let(:sign_in_page) { SignInPage.new }
      let(:password) { 'foobar' }

      scenario 'I want to sign in' do
        user = create(:user, password: password)

        sign_in_page.load
        sign_in_page.sign_in_as(user, password)

        expect(sign_in_page).to have_content t('user.signin.success')
      end
    end

Consideriamo ora l'eventualità di voler aggiungere una checkbox 'remember me' alla nostra form di login. Senza Page Objects dovremmo modificare ogni singolo test che richiede che 'remember me' sia stato "checkato", mentre con SitePrism bastaerà modificare il nostro Page Object:

    class SignInPage < Page
      set_url '/session/new'

      element :email, "input[name$='[email]']"
      element :password, "input[name$='[password]']"
      element :remember_me, "input[name$='[remember]']"
      element :submit_button, "input[type='submit']"

      def sign_in_as(user, password, remember=true)
        email_field.set user.email
        password_field.set password
        remember_me_field.set(remember)
        submit_button.click
      end
    end

Utilizzare i Page Objects quindi ci fa scrivere meno codice (soprattutto in fase di refactoring), riducendo quindi la possibilità di introdurre nuovi errori e facendoci risparmiare tempo.

Tedium

Stefano Verna (@steffoz) ha creato la gemma Tedium, basata su SitePrism, che rende ancora più semplice l'interazione con le form. Utilizzando Tedium, l'esempio precedente diventa:

    class SignInPage < SitePrism::Page
      set_url '/session/new'

      fields :email, :password
      submit_button

      submission :sign_in
    end

    feature 'As an user, in order to see my dashboard' do
      let(:sign_in_page) { SignInPage.new }

      scenario 'I want to sign in' do
        user = create(:user)

        sign_in_page.load
        sign_in_page.sign_in!(user.email, 'foobar')

        expect(sign_in_page).to have_content t('user.signin.success')
      end
    end

Grazie a Tedium possiamo risparmiarci la scrittura del selettore di ogni singolo field, e il codice necessario per la submission della form.

SimpleCov

SimpleCov serve a collezionare dati circa la coverage dei nostri test. In altre parole, riesce a dirci quale e quanto codice della nostra applicazione è stato eseguito dai test, dandoci una stima approssimativa di quanto la nostra applicazione sia testata.

Un aspetto interessante di SimpleCov riguarda la generazione di un report, che ci può dare un feedback visivo circa la coverage del nostro codice.

https://colszowka.github.io/simplecov/devise_result-0.5.3.pnghttps://colszowka.github.io/simplecov/devise_source_file-0.5.3.png

Questo risulta particolarmente utile per verificare di non esserci dimenticati di testare qualche path di esecuzione.

A Cantiere cerchiamo di mantenere la coverage superiore al 95%, evitando la ridondanza dei test (nel caso ideale ogni test dovrebbe eseguire una e una sola porzione specifica di codice).

Conclusioni

In questo articolo spero di essere riuscito a dare un'idea dell'approccio che adottiamo riguardo al testing dei nostri prodotti, e le motivazioni che ci hanno spinto a fare questo tipo di scelte. L'intento è quello di dimostrare che, con l'approccio e gli strumenti giusti, è possibile aggiungere del valore alle proprie applicazioni senza troppo sforzo.