Visualizzazione post con etichetta Rails. Mostra tutti i post
Visualizzazione post con etichetta Rails. Mostra tutti i post

giovedì 12 settembre 2013

Il DBB per il test della componente "Server" - parte 2


Prosecuzione del post sul BDD per il test del server

Il BDD - behavior-driven development (continua)

Definito lo scenario con cucumber, come abbiamo fatto nel post precedente, è necessario definire i test di validazione delle features, che si concentreranno sul "consumo" dei webservice e non sulla tecnica di programmazione.

I punti focali di questi test sono che:
  1. possiamo farli prima di iniziare a (o a far) codificare
  2. sono indipendenti dal linguaggio con cui sarà realizzato il progetto.
Per la definizione degli step possiamo partire dagli snippet generati automaticamente da cucumber (sul sito vi sono vari tutoriarl che illustrano come codificare gli step).
In questo caso, volendo scrivere un test di controllo su come un client esterno possa consumare i servizi esposti, non verrà verificata la logica dei modelli, ma il funzionamento dei controller. 

Ad esempio nel nostro case avremo:


Given(/^a user "(.*?)" with password "(.*?)"$/) do |user, password|
 @username=user
 @password= password
end

When(/^he logs whit username and password$/) do
  header 'Accept', 'application/json' 
  header 'Content-Type', 'application/json'
  params={username: @username, password: @password}
  post 'users/sign_in', params.to_json
  page = JSON.parse(last_response.body)
  @token=page["auth_token"]
  @token.should_not == nil
end

Then(/^he should navigate page "(.*?)" and see (\d+) records$/) do |arg1, arg2|
  get arg1, {auth_token: @token}
  page = JSON.parse(last_response.body)
  page.size.should == arg2.to_i
end
 
Then(/^he should navigate page "(.*?)"$/) do |arg1|
  get arg1, {auth_token: @token}
end

Then(/^he should see olny his documents$/) do 
  get "my_documents", {auth_token: @token}
  last_response.status.should == 200 #HTTP status OK
  page = JSON.parse(last_response.body)
  page.map{|u| u["user"]["customer_name"]}.compact.uniq.should include(@user.customer_name)
end

Then(/^he shouldn't navigate page "(.*?)"$/) do |arg1|
   get arg1, {auth_token: @token}
   last_response.status.should == 403 #HTTP status forbidden
end

Questo test verificherà - indipendentemente dalla modalità di realizzazione - che il nostro server risponda a: 
- un servizio di login
- un servizio di risposta agli indirizzi customers, operators, settings, documents, che verifichi il tocken ed il profilo dell'untente , eventualmente restituendo uno stato di fobidden e che restituisca una array di record
- un servizio denominato "my_documents"

Naturalmente alcuni di questi test, come ad esempio l'autenticazione con username e password, presuppongono che il sistema sia stato popolato. Basta predisporre un passo di popolazione dell'ambiante di test, attraverso una delle 2 modalità che preferiamo (ed in funzione di cosa dobbiamo testare):

A) utilizzo degli script di avvio di cucumber, in particolare dello svript env.rb, in cui, in questo caso ho aggiunto il blocco
    Before do
      User.create(:username=>"operator1", :password=>"test1234", :role=>"operator", :email=>"operator1@mail.local")#this code is run before each scenario
      User.create(:username=>"operator2", :password=>"test1234", :role=>"operator", :email=>"operator2@mail.local")#this code is run before each scenario
      User.create(:username=>"operator3", :password=>"test1234", :role=>"operator", :email=>"operator3@mail.local")#this code is run before each scenario
      cust1=User.create(:username=>"customer1", :password=>"test1234", :role=>"customer", :customer_name=>"azienda1", :email=>"customer1@mail.local")#this code is run before each scenario
      cust2=User.create(:username=>"customer2", :password=>"test1234", :role=>"customer", :customer_name=>"azienda2", :email=>"customer2@mail.local")#this code is run before each scenario
      cust1.documents.create(document_type: "F24_init")
      cust1.documents.create(document_type: "Bilancio_init")
      cust2.documents.create(document_type: "F24_init")
      cust2.documents.create(document_type: "Bilancio_init")
    end

B) scrittura di scenari di popolazione, che utilizzeremo per i test sui servizi.


Abbiamo quindi simulato - evitando di farlo manualmente ogni volta -
- l'apertura della finestra di login
- l'inserimento di user e password "amministrative"
- la navigazione (manuale, mancando al momento i link) di 4 pagine e il conteggio de record
- il logout
- l'inserimento di user e password "utente"
- la navigazione (manuale, mancando al momento i link) di 5 pagine e il conteggio de record

Considerando che al progredire di un progetto i dati da immettere per verificare quanto abbiamo codificato aumentano esponenzialmente... siamo ancora sicuri che scrivere test in questo modo sia una perdita di tempo?


lunedì 9 settembre 2013

Il DBB per il test della componente "Server" - parte 1


Prosecuzione del post sul paradigma Client/Server

Lo sviluppatore non testa ... falso! "auto-testa"

Come scritto sul post precedente, realizzare una componente server presuppone OBBLIGATORIAMENTE di investire delle risorse per il testing.

Ci sono vari modi di fare test, e per esperienza una volta che il codice viene scritto difficilmente si torna allo sviluppo del test.

Ma in realtà questo tempo viene già "sprecato". Facciamo un esempio.
Tipicamente lo sviluppo "agile" con ROR (o altri framework) prevedere che:
  1. si inizia a scrivere del codice, magari prendendo spunto da precendenti progetti o ricerche su google
  2. si avvia il server e si controlla se non si sono fatti errori di sintassi
  3. si avvia il browser e si naviga l'applicazione
  4. si compilano un po di dati
  5. si ricarica la pagina
  6. ci si imbatte in un errore
  7. trovato si vede sul log il punto sul sorgente che ha dato errore e si torna al punto 1

Quindi c'è già un tempo impiegato a di navigare, immettere dati, ricaricare pagine.

Inoltre più aumentano le funzionalità, maggiore è il tempo "perso" a caricare e ricaricare i dati.

Si pensi ad esempio a quante volte si ridigitano credenziali di accesso o testi di ricerca

Il BDD - behavior-driven development

Utilizzare correttamente i sistemi di BDD permettono
- di ottimizzare i tempi di sviluppo (riducendo i tempi di auto-test)
- di garantire la stabilità del codice
- di aumentare il livello di coinvolgimento del cliente e degli altri team

Nel nostro esempio, dovendo partire a sviluppare la componente server, si è partiti dalla definizione delle funzionalità (behavior) e degli scenari d'uso:

#encoding: utf-8
Feature: Login
  Per verificare che il login funzioni correttamente
  Come operatore devo poter accedere alle funzioni riservate
  Come cliente devo poter vedere solo i miei documenti
  Il sistema viene precaricato con 3 operatori, 2 utenti e 4 documenti
   
  Scenario: Login Operatore
    Given a user "operator1" with password "test1234"
    When he logs whit username and password
    Then he should navigate page "operators" and have 3 records 
    And  he should navigate page "customers" and have 2 records
    And  he should navigate page "settings"
    And  he should navigate page "documents" and have 4 records

  Scenario: Login Cliente
    Given a user "customer1" with password "test1234"
    When he logs whit username and password
    Then he should see olny his documents
    And  he shouldn't navigate page "documents" 
    And  he shouldn't navigate page "customers" 
    And  he shouldn't navigate page "users"
    And  he shouldn't navigate page "settings"

e ancora

#encoding: utf-8
Feature: Caricamento documenti
  Per verificare il corretto funzionamento dei documenti
  Come operatore dovrei essere in grado di caricare un nuovo tipo di documento ed   associarlo ad un cliente
  Come Cliente dovrei essere in grado di vedere i documenti a me associati e scaricarli
Il sistema viene precaricato con 3 operatori, 2 utenti e 4 documenti
Scenario: Caricamento documenti da operatore1
    Given a user "operator1" with password "test1234"
    When he logs whit username and password
    Then he should navigate page "customers"
    And he should navigate customer "azienda1"
    And he should upload an "F24" using "file1"
    And customer should have assigned an "F24"

  Scenario: Caricamento documenti da operatore2
    Given a user "operator2" with password "test1234"
    When he logs whit username and password
    Then he should navigate page "customers"
    And he should navigate customer "azienda2"
    And he should upload an "Bilancio" using "file2"
    And customer should have assigned an "Bilancio"

  Scenario: Accesso a documenti da cliente
    Given a user "customer1" with password "test1234"
    When he logs whit username and password
    Then he should have a list of documents that include an "F24_init"

Paradigma Client/Server anche per sviluppare applicazioni "semplici"? Perchè no?

Oggi ho iniziato a sviluppare una piccola applicazione - rigorosamente con Rails -  ad uso interno.

Nulla di complicato, anzi tutt'altro, ma mi sono chiesto se la sua semplicità fosse un buon motivo per non svilupparla "bene", ovvero per non dividere i livelli di logica di business e di presentazione.

Non è che le cose piccole siano per definizioni da fare male, anzi .... la semplicità (così ci insegna la UX) spesso è figlia di molto lavoro ed esperienza.

Veniamo quindi alla nostra applicazione.

Le funzioni richieste sono basilari, e dopo una prima fase di analisi sono riassumibili in:
- deve prevedere accessi profilati ed autenticati
- deve permettere la gestione di settings
- deve permettere la il caricamento di documenti con alcuni metadati (legati agli utenti ed ai setting)
- deve inviare delle notifiche a fronte del caricamento
- deve tracciare le azioni eseguite sul documento

Abbiamo quindi il modello principale DOCUMENT a cui sono funzionali il modello USER ed il modello SETTING

La prima applicazione gestirà la logica di business e le funzioni backend del sistema e sarà interrogabile solo tramite WebService

La seconda applicazione - che potrà anche essere client javascript - consumerà tali servizi e fornirà la User Interface all'utente, definendo la logica di presentazione

I vantaggi - a parte quelli puramente "accademici" - sono molteplici:

1) Possiamo demandare ad un'altro gruppo / persona lo sviluppo dell'interfaccia
2) Possiamo scrivere differenti interfacce per differenti device. Quindi realizzare da un client locale, compilato per la singola piattaforma, ad una webapp, ad un sito mobile
3) Il gruppo che si occupa delle interfacce utente NON POTRA' in alcun modo interferire con la logica di business
4) Nessun codice sarà condiviso tra i due gruppi e pertanto sarà effettivamente possibile DIMEZZARE i tempi di sviluppo (lo so non è vero.. ma passatemi la licenza), a patto di definire bene la struttura dei messaggi

Svantaggi:

1) senza una suite di test chi sviluppa il backend lavorerà alla cieca o dovrà fare dello sviluppo "a perdere".
2) senza una formalizzazione dei messaggi chi lavora al frontend sarà costretto a lavorare senza dati in modo statico

A mio parere questi non sono degli svantaggi, ma una evidente forzatura a costringere lo sviluppatore in termini di ciò che può essere TESTATO e non di ciò che può essere VISTO

lunedì 18 febbraio 2013

Heroku toolbelt e Rails Installer: qualche problema di convivenza....

Chi dovesse aver avuto problemi dopo l'installazione di Heroku Toolbelt e Rails Installer su piattaforma windows, potrebbe innanzitutto controllare i path impostati dai due sistemi.

Con estrema cura Heroku installa la propria versione di Ruby e le proprie gemme ....  peccato si perda il supporto di DevKit installato con RailsInstaller.

Per ovviare al problema basta rimuovere dalla variabile PATH i percorsi di Ruby creati da Heroku

(nel mio caso)  C:\Program Files (x86)\ruby-1.9.2\bin,



mercoledì 13 febbraio 2013

Rails e le Nested resources

Le “nested Resource” e le relative “Nested form” sono dei costrutti che permettono di aggiornare facilmente, con un unico submit da parte del browser, un modello e degli altri modelli ad esso collegati:

--- NB: L’obiettivo di questo articolo è spiegare la logica di ciò che c’è dietro le nested re source, ovvero il meccanismo in base a cui RoR permette di gestire le risorse ---

Le caratteristiche del framework Rails di "Convention over Configuration" rendono le applicazioni RoR "magiche" agli occhi di chi non lo conosce.

In realtà non c'è nulla di magico in tutto questo, ma si tratta di rispettare alcune convenzioni. Convenzioni che evolvono, spesso alla ricerca di maggiore protezione e sicurezza (vedi la direttiva attr_accessible) o rispettando il concetto di base che la maggioranza della community e degli sviluppatori fanno così.

Una delle "magie" che mi interessa discutere qui è quella delle nested resource, ma per completezza dobbiamo fare prima qualche passo indietro.

La creazione o l'aggiornamento di un modello fa largo uso degli hash. Ad esempio posso scrivere (ipotizzando che esista il team con id 1)
giocatore = Player.new(:firstname=>"Nome", :surname => "Cognome", :team_id=>"1")
oppure
parametri={:firstname=>"Nome", :surname => "Cognome", :team_id=>"1"}
giocatore= Player.new(parametri)
l’ActionController implementa questo concetto quando estrae dall’Hash params i dati con cui costruire le istanze dei modelli. Rispettando le convezioni RoR nei controller generati con lo scaffold ritroviamo
@player = Player.new(params[:player]) # nel create
@player.update_attributes(params[:player]) # nell'update
Il form HTML infatti è stato generato utilizzando il “model_name” come prefisso, e i dati saranno inviati nel formato:
player[firstname]=Nome;player[lastname]=Cognome;player[team_id]=1;commit=Salva.....
generando un hash di questo tipo
Parameters: {"utf8"=>"", "authenticity_token"=>".....", "player"=>{"lastname"=>"Cognome", "firstname"=>"Nome", "team_id"=>"1"}, "commit"=>"Update Player", "id"=>"1"}
Diventa chiaro come params[:player] rappresenti esattamente la variabile parametri che avevamo instanziato prima.
Arriviamo alle nested resource. Ipotizziamo 2 modelli, Team e Player in cui abbiamo le seguenti definizioni:
 class Team < ActiveRecord::Base
    attr_accessible :name
    has_many :players
 end
 class Player < ActiveRecord::Base
    attr_accessible :firstname, :lastname, :team_id
    belongs_to :team
 end

Immaginando che ci sia la seguente struttura
Team
id
name
1
team_1
2
team_2
Player
id
firstname
lastname
team_id
1
nome1
cognome1
1
2
nome2
cognome2
2
3
nome3
cognome3
2
4
nome4
cognome4
1
ActiveRecord genera, a seguito delle definizioni “has_many” e “belongs_to” dei metodi dinamici che rappresentano l’oggetto Team o le istanze di Player collegate
In particolare abbiamo a disposizione 2 metodi di instanza: “players” e “players=”. Vediamone l’uso
>> t=Team.first
Team Load (1.0ms)  SELECT "teams".* FROM "teams" LIMIT 1
#<Team id: 1, name: "team1", created_at: "2013-02-11 11:55:01", updated_at: "2013-02-11 11:55:01">
   
>> t.players
[#<Player id: 1, lastname: "cognome1", firstname: "nome1", team_id: 1, created_at: "2013-02-11 11:55:46", updated_at: "2013-02-11 11:55:46">, #<Player id: 2, lastname: "cognome2", firstname: "nome2", team_id: 1, created_at: "2013-02-11 11:55:57", updated_at: "2013-02-11 11:55:57">]
Player Load (1.0ms)  SELECT "players".* FROM "players" WHERE "players"."team_id" = 1

>> t.players=[]
SQL (1.0ms)  UPDATE "players" SET "team_id" = NULL WHERE "players"."team_id" = 1 AND "players"."id" IN (1, 2)
 []

Il metodo “players” risulta abbastanza naturale, ovvero restituisce un array con tutte le istanze di Player collegate al Team che stiamo esaminando. In base alla stessa convenzione, “players"=” setta l’array, e nel caso in cui gli si passi un array vuoto automaticamente elimina tutte le istanze precedentemente associate.
Pertanto se avessimo predisposto un array di player e lo avessimo passato al metodo palyers= avremmo ottenuto questo:
>> pl_array=[Player.new(:firstname=>"nome1",:lastname=>"cognome1"), Player.new(:firstname=>"nome2", :lastname=>"cognome2")]
[#<Player id: nil, lastname: "cognome1", firstname: "nome1", team_id: nil, created_at: nil, updated_at: nil>, #<Player id: nil, lastname: "cognome2", firstname: "nome2", team_id: nil, created_at: nil, updated_at: nil>]
   
>> t.players=pl_array
(0.0ms)  begin transaction
SQL (1.0ms)  UPDATE "players" SET "team_id" = NULL WHERE "players"."team_id" = 1 AND "players"."id" IN (4, 5)
SQL (0.0ms)  INSERT INTO "players" ("created_at", "firstname", "lastname", "team_id", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 12 Feb 2013 08:06:48 UTC +00:00], ["firstname", "nome1"], ["lastname", "cognome1"], ["team_id", 1], ["updated_at", Tue, 12 Feb 2013 08:06:48 UTC +00:00]]
SQL (0.0ms)  INSERT INTO "players" ("created_at", "firstname", "lastname", "team_id", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 12 Feb 2013 08:06:48 UTC +00:00], ["firstname", "nome2"], ["lastname", "cognome2"], ["team_id", 1], ["updated_at", Tue, 12 Feb 2013 08:06:48 UTC +00:00]]
(84.0ms)  commit transaction
[#<Player id: 6, lastname: "cognome1", firstname: "nome1", team_id: 1, created_at: "2013-02-12 08:06:48", updated_at: "2013-02-12 08:06:48">, #<Player id: 7, lastname: "cognome2", firstname: "nome2", team_id: 1, created_at: "2013-02-12 08:06:48", updated_at: "2013-02-12 08:06:48">]

Notare come sia stata prima annullata la presenza di eventuali record e poi inseriti in massa tutti i valori. Inoltre andando ad analizzare il contenuto di pl_array in cui registrato due nuove instanze di player notiamo che le istanze sono correttaemente associate a record del db:
>> pl_array
[#<Player id: 8, lastname: "cognome1", firstname: "nome1", team_id: 1, created_at: "2013-02-12 08:46:39", updated_at: "2013-02-12 08:46:39">, #<Player id: 9, lastname: "cognome2", firstname: "nome2", team_id: 1, created_at: "2013-02-12 08:46:39", updated_at: "2013-02-12 08:46:39">]

Ripetendo l’operazione t.players=pl_array non otteniamo nessun risultato in quanto le istanze sono già associate con il team.
Pertanto se nel controller implementassimo la creazione di un array avremmo ottenuto di salvare modello principale (team) e modelli collegati (players) con un unico form
La direttiva accepts_nested_attributes_for semplifica questo passaggio, generando un nuovo metodo, MODELNAME_attributes= , che nasconde tutti i meccanismi di definizione dell’array di risorse:
     class Team < ActiveRecord::Base
    attr_accessible :name, :players_attributes
    has_many :players
    accepts_nested_attributes_for :players
end
non abbiamo più la necessità di generare noi le istanze dei modelli collegati, ma semplicemente di passare un hash o un array di hash contenenti gli attributi da aggiornare:
>> pl_attributes = [{ :firstname => 'nome1', :lastname=>'cognome1'}, { :firstname => 'nome2', :lastname=>'cognome2'},{ :firstname => 'nome3', :lastname=>'cognome3'},{ :firstname => 'nome4', :lastname=>'cognome4'}]
  [{:firstname=>"nome1", :lastname=>"cognome1"}, {:firstname=>"nome2", :lastname=>"cognome2"}, {:firstname=>"nome3", :lastname=>"cognome3"}, {:firstname=>"nome4", :lastname=>"cognome4"}]
   
>> t.players
[#<Player id: 8, lastname: "cognome1", firstname: "nome1", team_id: 1, created_at: "2013-02-12 08:46:39", updated_at: "2013-02-12 08:46:39">, #<Player id: 9, lastname: "cognome2", firstname: "nome2", team_id: 1, created_at: "2013-02-12 08:46:39", updated_at: "2013-02-12 08:46:39">]
  
>> t.players_attributes=pl_attributes
[{:firstname=>"nome1", :lastname=>"cognome1"}, {:firstname=>"nome2", :lastname=>"cognome2"}, {:firstname=>"nome3", :lastname=>"cognome3"}, {:firstname=>"nome4", :lastname=>"cognome4"}]
   
>> t.save
(0.0ms)  begin transaction
SQL (3.0ms)  INSERT INTO "players" ("created_at", "firstname", "lastname", "team_id", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00], ["firstname", "nome1"], ["lastname", "cognome1"], ["team_id", 1], ["updated_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00]]
SQL (1.0ms)  INSERT INTO "players" ("created_at", "firstname", "lastname", "team_id", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00], ["firstname", "nome2"], ["lastname", "cognome2"], ["team_id", 1], ["updated_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00]]
SQL (0.0ms)  INSERT INTO "players" ("created_at", "firstname", "lastname", "team_id", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00], ["firstname", "nome3"], ["lastname", "cognome3"], ["team_id", 1], ["updated_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00]]
SQL (1.0ms)  INSERT INTO "players" ("created_at", "firstname", "lastname", "team_id", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00], ["firstname", "nome4"], ["lastname", "cognome4"], ["team_id", 1], ["updated_at", Tue, 12 Feb 2013 09:14:35 UTC +00:00]]
(94.0ms)  commit transaction
  
>> t.players
[#<Player id: 8, lastname: "cognome1", firstname: "nome1", team_id: 1, created_at: "2013-02-12 08:46:39", updated_at: "2013-02-12 08:46:39">, #<Player id: 9, lastname: "cognome2", firstname: "nome2", team_id: 1, created_at: "2013-02-12 08:46:39", updated_at: "2013-02-12 08:46:39">, #<Player id: 10, lastname: "cognome1", firstname: "nome1", team_id: 1, created_at: "2013-02-12 09:14:35", updated_at: "2013-02-12 09:14:35">, #<Player id: 11, lastname: "cognome2", firstname: "nome2", team_id: 1, created_at: "2013-02-12 09:14:35", updated_at: "2013-02-12 09:14:35">, #<Player id: 12, lastname: "cognome3", firstname: "nome3", team_id: 1, created_at: "2013-02-12 09:14:35", updated_at: "2013-02-12 09:14:35">, #<Player id: 13, lastname: "cognome4", firstname: "nome4", team_id: 1, created_at: "2013-02-12 09:14:35", updated_at: "2013-02-12 09:14:35">]

Possiamo notare le seguenti cose
-          Non è stato necessario creare i modelli degli oggetti, ma abbiamo dovuto definire solo un hash
-          L’assegnazione non ha scatenato, come prima, il salvataggio, ma è stato necessario salvare manualmente il modello principale
-          I dati passati al metodo non hanno sovrascritto quelli esistenti ma sono andati in aggiunta

La direttiva accepts_nested_attributes_for accetta anche delle ulteriori opzioni, che permettono di definire meglio il suo comportamento:
-          :allow_destroy – se impostato a true permette di utilizzare un attributo “_destroy”, valorizzato con true, per eliminare i record
-          :reject_if – permette di definire le condizioni in base alle quali scartare la generazione di un record
-          :limit – permette di definire un limite al numero di record processati

Analizzando meglio il modo in cui deve essere definito l’hash, e ricordando come lavora il metodo params, per ottenere una definizione @team.players_attributes=… ..  dobbiamo generare un passaggio di parametri come il seguente
team[players_attributes][1][firstname]=Nome1; team[players_attributes][1][lastname]=Cognome1;
team[players_attributes][2][firstname]=Nome2; team[players_attributes][2][lastname]=Cognome2;
team[players_attributes][3][firstname]=Nome3; team[players_attributes][3][lastname]=Cognome3; commit=Salva.....
nel caso di aggiornamento avremo
team[players_attributes][1][id]=1;team[players_attributes][1][firstname]=Luca; team[players_attributes][1][lastname]=Arcara; … commit=Salva.....
nel caso di cancellazione avremo
 team[players_attributes][1][id]=1;team[players_attributes][1][_destroy]=1;…;commit=Salva.....

il codice Rails che gestisce il rendering dei campi è il seguente:
<%= form_for(@team) do |f| %>
  <% if @team.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@team.errors.count, "error") %> prohibited this team from being saved:</h2>
      <ul>
        <% @team.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
    
  <div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </div>
  <%=f.fields_for :players do |p|%>
    <p>Giocatore:
      <%=p.text_field :firstname%>
      <%=p.text_field :lastname%>
      <%=p.check_box "_destroy"%> Elimina
    </p>
  <%end%>
  <p>Nuovo Giocatore:
     <%=text_field_tag "team[players_attributes][#{@team.players.count}][firstname]"%>
     <%=text_field_tag "team[players_attributes][#{@team.players.count}][lastname]"%>
  </p>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

NB: La sezione riguardante il nuovo giocatore può essere gestita anche in altro modo, ad esempio  creando un nuovo oggetto e verificando l’attributo new_record?, oppure utilizzando un po’ di javascript ed ajax per creare l’oggetto ed i corrispondenti tag HTML (un ottimo esempio lo trovate qui http://railscasts.com/episodes/196-nested-model-form-part-1?view=asciicast

domenica 16 dicembre 2012

BDD: dare valore alle feature (e vivere felici !)

L'acronimo DBB sta per "Behaviour-Driven Development", ovvero  - semplificando - definire priorità e feature da sviluppare in funzione del comportamento che si vuole un determinato sistema abbia.

Per chi non conoscesse il BDD, un articolo che merita di essere letto credo sia questo. E0' un viaggoi molto interessante nelle motivazioni che hanno portato allo sviluppo di queste tecniche. 

Partiamo dal presupposto che un progetto che preveda sin dalla nascita con unit test, funcional test ecc ecc ecc è sicuramente un progetto soldo ed affidabile. Magari fallimentare - aggiungo io - ma di sicuro solido.

Sul perchè fallimentare è inutile dilungarsi troppo... è una questione di principi e punti di vista. 

Come su un ring: In un angolo c'è chi crede che si possa, a "tempo 0" identificare tutte le possibili opzioni e comportamenti del sistema, nell'altro chi pensa che cosa fa un sistema lo si potrà scoprire solo quando lo sviluppo sarà terminato.. e lo sviluppo non terminerà MAI!

Eccessi a parte, occorre capire l'utilità (in ogni singolo contesto) dei test per poter capire quale è l'approccio più corretto: 
- è uno sviluppo modulare, magari con tante risorse, ed è necessario capire impatti delle singole modifiche,fare regression test, verificare periodicamente la stabilità del sistema?
- è uno sviluppo "one man show", dove si rischia di passare settimane per quello stupendo effetto che fa scomparire il box che decideremo di eliminare in un secondo momento?
- è uno sviluppo "a perdere", magari per fare delle prove o sperimentare delle alternative in modo prototipale?
Eccetra ... eccetra .... eccetra

Insomma ogni progetto ha delle sue caratteristiche, ma ce ne sono alcune che sono comuni a tutti:
- il sistema dovrà avere delle funzionalità - fare delle "cose"
- queste funzionalità ci sono non perchè sono "fiche", ma perchè hanno un valore per il "cliente"

Il DBB si focalizza su questi due punti. Richiede che le funzionalità siano descritte attraverso delle storie che identificano il Come, il Cosa ed il Perchè:


As a Role 
I request a Feature
To gain a Benefit

ovvero

Come RUOLO
Voglio che il sistema FACCIA QUESTO
Per ottenere un VALORE / OBIETTIVO

Anche se sembra una banalità, costringere il cliente ( o noi stessi) ad associare ad ogni funzione un valore costringe a valutare attentamente
- se quella funzionalità davvero serve
- se quella funzionalità può essere fatta in modo diverso

ATTENZIONE!!! Questo non vuol dire che devono essere descritti tutti i pulsanti , finestre, campi, azioni ecc ecc ecc del sistema. Ci si focalizza sulle funzioni ritenute importanti e di valore, e pian piano si aggiungono o modificano.
Ma su questo punto ci torneremo, anche con un post dedicato a come applicare il DBB a progetti già in corso...

Un esempio -  preso dal sito di cucumber - descrive come alcune funzioni banali possano o meno essere associate ad un preciso valore di business, parla della funzione di Login.
a che serve il login? o meglio: Che VALORE ha il login?

In effetti se in un certo stato del progetto non siamo in grado di trovare un valore alla funzione di "login", allora non vale la pena svilupparla (in quel momento)

Ma andando ad analizzare meglio la cosa, si può dire che il login server
- perchè ogni utente sia in grado di vedere i suoi ordini così da poterli inviare e far generare profitti
- perchè ogni utente possa personalizzare il suo ambente, così da fidelizzarsi, tornare spesso e permettere una maggiore impression di pubblicità
- perchè il sistema sia in grado di identificare li interessi dell'utente e proporre i giusti suggerimenti e link a prodotti / pubblicità

Se avessimo pensato ad un sistema che non deve tenere traccia di queste funzioni, il login, sebbene possa essere sempre utile per fornire servizi aggiuntivi, non sarebbe stato così indispensabile da identificare subito un suo scenario.

Un altro aspetto interessante del DBB è che, descrivendo i comportamenti, permette di far evolvere i progetti in funzione delle reali necessità, e di far emergere richieste in contrasto con le linee strategiche dell'applicazione (quante volte è capitato di ricevere delle richieste che non avevano senso?)

L'inserimento di un nuovo comportamento non dovrebbe portare problemi agli altri. Il condizionale è d'obbligo perché a volte nuovi comportamenti causano degli effetti "collaterali" ed in tal caso è fondamentale capire se l'effetto è giusto - e quindi occorre rivedere la definizione dei comportamenti -  o se c'è un errore nel come il sistema risponde.

Facciamo un esempio.
Stiamo sviluppando un sito con elenchi di prodotti e navigazioni avanzate, ma poichè non facciamo vendita online non è prevista una funzione di login. I nostri scenari quindi non prevedono un "ruolo" dell'utente

In un secondo momento si decide di attivare un "e-commerce" e la relativa funzione di login. I precedenti scenari vanno in errore perché l'autenticazione è stata sviluppata prevedendo un login obbligatorio, e pertanto l'utente "guest" non riesce più a vedere i cataloghi

La presenza dell'errore deve necessariamente essere discussa con il cliente, perché è necessario capire se il vecchio comportamento è valido e mantiene inalterato il suo valore di business (magari il cliente vorrebbe che gli ospiti potessero vedere solo i prodotti e non più i prezzi)


Fin qui la teoria... vedremo successivamente alcune regole su come scrivere le "storie" e su come prdisporre i programmi di controllo

martedì 6 novembre 2012

Uhuru AppCloud: Installare FatFreeCRM come Apps - Parte 2

 Precedenti Post:


Come detto nel precedente post, la pubblicazione sulla nostra Uhuru- nuvola richiede alcuni accorgimenti, accorgimenti che per essere applicati richiedono necessariamente un ambiente di sviluppo funzionante e configurato.
Nel nostro esercizio però partiremo da zero, anche per verificare le possibilità offerte dai servizi cloud di essere utilizzate anche solo come file server o db server.
Quindi ci appresteremo a:
-          Creare un db in remoto da utilizzare per la configurazione “production” di fatfree
-          Apportare tutte le modifiche a FatFree per utilizzare il db remoto e per prepararlo alla pubblicazione
Per proseguire è necessario che
-          Ruby, Git e Rails siano correttamente installati e configurati
-          Mysql 5 sia installato e funzionante sul client (è necessario per poter compilare la gemma mysql2)

Per questo post ho utilizzato RailsInstaller e l’archivio mysql-5.5.28-win32.zip
Colleghiamoci alla nostra nuvola (vedi post precedente) e verifichiamo i servizi attivi:
vmcu service


otterremo una risposta come questa:

C:\Users\Luca>vmcu services

>         ============== System Services ==============
>          
>         +------------+---------+---------------------------------------+
>         | Service    | Version | Description                           |
>         +------------+---------+---------------------------------------+
>         | mongodb    | 2.0     | MongoDB NoSQL store                   |
>         | mssql      | 2008    | MS SQL Server database service        |
>         | mysql      | 5.1     | MySQL database service                |
>         | postgresql | 9.0     | PostgreSQL database service (vFabric) |
>         | rabbitmq   | 2.4     | RabbitMQ message queue                |
>         | redis      | 2.2     | Redis key-value store service         |
>         | uhurufs    | 0.9     | Uhuru persistent filesystem service   |
>         +------------+---------+---------------------------------------+
>          
>         =========== Provisioned Services ============



Creiamo il nostro database:

>         Vmcu create-service mysql ff-development



Creiamo un tunnel così da poter utilizzare il database remoto *** QUESTA SHELL DOVRA’ RIMANERE SEMPRE APERTA ***:

>         Vmcu tunnel 
>          
>         1: ff-development
>         Which service to tunnel to?: 1
>         Deploying tunnel application 'caldecott'.
>         Uploading Application:
>           Checking for available resources: OK
>           Packing application: OK
>           Uploading (1K): OK
>         Push Status: OK
>         Binding Service [ff-development]: OK
>          
>         Staging Application 'caldecott': OK
>          
>         Starting Application 'caldecott': OK
>         Getting tunnel connection info: OK
>          
>         Service connection info:
>           username : uecvMF7JNEM1u
>           password : p1l4sYafcOFVq
>           name     : d9818ba918245401c85e2e6efd4ceba85
>          
>         Starting tunnel to ff-development on port 10000.
>         1: none
>         2: mysql
>         3: mysqldump
>         Which client would you like to start?: 1
>         Open another shell to run command-line clients or
>         use a UI tool to connect using the displayed information.
>         Press Ctrl-C to exit...


Effettuiamo il download di mysql-5.5.28-win32.zip e scompattiamo l’archivio in c:\mysql5. Quindi installiamo la gemma mysql2 versione 0.3.1

>         gem install --no-rdoc --no-ri mysql2 -v 0.3.10 -- with-mysql-dir=c:\mysql5

quindi copiamo il file libmysql.dll nella cartella bin del nostro interprete ruby. Ad esempio

>         copy C:\mysql5\lib\libmysql.dll C:\RailsInstaller\Ruby1.9.3\bin

Verificare con un client MySQL il collegamento al servizio. ad esempio con SquirrerSQL la stringa sarà:

jdbc:mysql://localhost:10000/ d9818ba918245401c85e2e6efd4ceba85