Perché utilizzare gli Unit Tests?
Mentre guidi per andare al lavoro una mattina, hai una improvvisa intuizione riguardo all’applicazione su cui stai lavorando. Ti rendi conto che puoi implementare una modifica che ottimizzerà drasticamente l’applicazione. Potrebbe trattarsi di un refactoring che rende il nostro codice più leggibile, aggiunge una nuova funzionalità o corregge un bug.
La domanda che ti viene spontanea è: “Quanto è sicuro apportare questo miglioramento?” E se il cambiamento ha effetti collaterali indesiderati? La modifica sarà anche semplice e richiede pochi minuti, ma se ci volessero ore di test manuali su tutti gli scenari applicativi? E se te ne dimentichi uno e l’applicazione va in produzione? L'intuizione che hai avuto vale il rischio?
Gli unit test automatizzati possono fornire una maggiore sicurezza che ti consente di migliorare costantemente le tue applicazioni e di non temere il codice su cui stai lavorando. Avere test automatizzati che verificano rapidamente la funzionalità ti permette di controllare con sicurezza e di apportare miglioramenti che altrimenti non avresti osato fare. Ti aiutano inoltre a creare soluzioni a lungo termine: il che costituisce un notevole ritorno sull’investimento.
Il framework NET rende facile e naturale adottare unit test e consente Test Driven Development workflow che garantisce a sua volta lo sviluppo basato su test first.
Vediamo un semplice esempio con Visual Studio 2008 Professional:
Creiamo una semplice tabella in SQL Express:
Ora aggiungiamo al progetto un LINQ to SQL Classes:
Ora con un semplice drag portiamo la tabella sul designer dell'item inserito. Con questa operazione verrà creata una classe usando lo schema della tabella (le proprietà della classe mappano le colonne della tabella).
In automatico, poichè la tabella si chiama Persone, verrà creata una classe di nome persone (è possibile comunque cambiare i nomi sia delle tabelle che delle proprietà).
Ora aggiungiamo un nostro metodo alla classe. Per questo ci viene incontro la parola chiave partial che permette di aggiungere metodi, proprietà ed eventi ad una classe già esistente senza toccarla.
partial class Persone
{
public bool IsValid()
{
return (this.Cognome != string.Empty) && (this.Nome != string.Empty);
}
}
Ora creiamo una classe di helper per gestire le persone:
public class PersoneRepository{
private dcPersoneDataContext db = new dcPersoneDataContext();
public IQueryable<Persone> Persone() {
return db.Persones;
}
public IQueryable<Persone> ListPersoneDaCitta(string Citta)
{
return (from persona in db.Persones
where persona.Città == Citta
orderby persona.Cognome
select persona).AsQueryable();
}
public Persone GetPersona(Guid id) {
return db.Persones.SingleOrDefault(d => d.Id == id);
}
public void Add(Persone persona)
{
db.Persones.InsertOnSubmit(persona);
}
public void Delete(Persone persona)
{
db.Persones.DeleteOnSubmit(persona);
}
public void Save() {
db.SubmitChanges();
}
}
Ora alla soluzione aggiungiamo un progetto di test:
E aggiungiamo i seguenti metodi:
[TestClass]
public class PersoneTest
{
[TestMethod]
public void PersoneNonCorrettoQuandoUnaProprietàNonèCorretta()
{
Persone persona = new Persone()
{
Nome="Nico",
Cognome ="",
Telefono="10212092340",
CAP="20052",
Indirizzo="Via Lario 10",
Città="Monza"
};
bool isValid = persona.IsValid();
Assert.IsFalse(isValid);
}
[TestMethod]
public void PersoneCorrettoQuandoTutteProprietàSonoCorrette()
{
Persone persona = new Persone()
{
Nome = "Nico",
Cognome = "Ciavarella",
Telefono = "10212092340",
CAP = "20052",
Indirizzo = "Via Lario 10",
Città = "Monza"
};
bool isValid = persona.IsValid();
Assert.IsTrue(isValid);
} }
In questo caso testiamo quando una proprietà non è corretta (non è stato assegnato il cognome) e quando tutte le proprietà sono formalmente corrette. Nel metodo IsValid() controlliamo che il nome ed il cognome non siano vuoti.
Il nome del singolo test deve essere molto esplicativo visto che in un progetto potresti avere migliaia di piccoli test. Usa un naming pattern tipo "Noun_Should_Verb".
Quando scriviamo dei test dobbiamo evitare di avere singoli test che fanno troppe cose.
E' buona norma invece che ogni singolo test verifichi un solo concetto (anche perchè facilita la causa del fallimento di un singolo test). Una buona pratica per realizzare questo è avere un singolo assert per ogni test. Se hai più di un assert in un metodo di test, assicurati che questi rappresentino lo stesso concetto. Se hai dei dubbi, fai un altro test.
Ora da Visual Studio lanciamo tutti i test della soluzione:
Ora aggiungiamo al nostro progetto una classe che utilizza la PersoneRepository
public class PersoneController
{
PersoneRepository m_personeRepository;
public PersoneRepository PersoneRepository
{
get { return m_personeRepository; }
set { m_personeRepository = value; }
}
public PersoneController()
{
m_personeRepository = new PersoneRepository();
}
public PersoneController(PersoneRepository repository)
{
m_personeRepository = repository;
}
}
e nel progetto di test aggiungiamo un nuovo metodo che verifica se il ListPersoneDaCitta è corretto:
PersoneRepository getPersoneRepository()
{
Persone persone = new Persone() { Nome = "Nico", Cognome = "Ciavarella", Telefono = "10212092340", CAP = "20052", Indirizzo = "Via Lario 10", Città = "Monza" };
PersoneRepository p = new PersoneRepository();
p.Add(persone);
return p;
}
[TestMethod]
public void ListaPersoneInCittaCorretto()
{
PersoneController p = new PersoneController(getPersoneRepository());
var j = p.PersoneRepository.ListPersoneDaCitta("Monza");
Assert.IsTrue(j.Count() == 1);
}
Se lanciamo tutti i test del progetto avremo:
Se guardiamo al messaggio di errore, vediamo che il test fallisce perchè la nostra classe PersoneRepository nel progetto di test non è abilitata a connettersi al database. La nostra applicazione si sta connettendo ad un SQL Server Express locale. Potremmo modificare nel nostro progetto di test aggiungendo il database SQL Express e aggiungendo nell'App.Config la connection string. In questo modo il nostro test verrebbe eseguito correttamente.
Utilizzare reali connessioni al database negli unit test comporta una serie di sfide:
- l'esecuzione di unit test rallenta significativamente. Più sono lenti, meno frequentemente avvieremo la loro esecuzione. Idealmente noi vorremmo che fossero velocissimi e che fossero come compilare un progetto.
- noi vorremmo che ogni unit test fosse isolato ed indipendente dagli altri (nessun effetto di dipendenza). Quando lavoriamo con database reali, dovremmo sempre considerare il loro stato e reimpostarlo ad ogni avvio di test. In questo caso ci viene incontro il pattern chiamato "dependency injection", che aiuta a lavorare intorno al problema e ad evitare di lavorare con database reali nei test.
Dependency injection
In pratica dobbiamo disaccoppiare la nostra classe PersoneRepository che accede al database:
Estraiamo dalla classe l'interfaccia relativa:
Ora la nostra classe implementa l'interfaccia IPersoneRepository:
public class PersoneRepository : IPersoneRepository
{
private dcPersoneDataContext db = new dcPersoneDataContext();
public IQueryable<Persone> Persone() {
return db.Persones;
}
public IQueryable<Persone> ListPersoneDaCitta(string Citta)
{
return (from persona in db.Persones
where persona.Città == Citta
orderby persona.Cognome
select persona).AsQueryable();
}
public Persone GetPersona(Guid id) {
return db.Persones.SingleOrDefault(d => d.Id == id);
}
public void Add(Persone persona)
{
db.Persones.InsertOnSubmit(persona);
}
public void Delete(Persone persona)
{
db.Persones.DeleteOnSubmit(persona);
}
public void Save() {
db.SubmitChanges();
}
}
Questa è la definizione dell'interfaccia:
public interface IPersoneRepository
{
void Add(Persone persona);
void Delete(Persone persona);
Persone GetPersona(Guid id);
System.Linq.IQueryable<Persone> ListPersoneDaCitta(string Citta);
System.Linq.IQueryable<Persone> Persone();
void Save();
}
Modifichiamo di conseguenza anche la nostra classe che utilizza la PersoneRepository:
public class PersoneController
{
IPersoneRepository m_personeRepository;
public IPersoneRepository PersoneRepository
{
get { return m_personeRepository; }
set { m_personeRepository = value; }
}
public PersoneController()
{
m_personeRepository = new PersoneRepository();
}
public PersoneController(IPersoneRepository repository)
{
m_personeRepository = repository;
}
}
Ora nel progetto di test creiamo una classe 'finta' che simula il database:
public class FakePersoneRepository : IPersoneRepository
{
private List<Persone> personeList;
public FakePersoneRepository(List<Persone> persone)
{
personeList = persone;
}
#region IPersoneRepository Members
public void Add(Persone persona)
{
personeList.Add(persona);
}
public void Delete(Persone persona)
{
personeList.Remove(persona);
}
public Persone GetPersona(Guid id)
{
return personeList.SingleOrDefault(d => d.Id == id);
}
public IQueryable<Persone> ListPersoneDaCitta(string Citta)
{
return (from p in personeList
where p.Città == Citta
orderby p.Cognome
select p).AsQueryable();
}
public IQueryable<Persone> Persone()
{
return personeList.AsQueryable();
}
public void Save()
{
foreach (Persone p in personeList)
{
if (!p.IsValid())
throw new ApplicationException("Persona non valida!");
}
}
#endregion
}
Ed infine modifichiamo il nostro test precedente che falliva:
IPersoneRepository getPersoneRepository()
{
Persone persone = new Persone() { Nome = "Nico", Cognome = "Ciavarella", Telefono = "10212092340", CAP = "20052", Indirizzo = "Via Lario 10", Città = "Monza" };
List<Persone> ListPersone = new List<Persone>();
ListPersone.Add(persone);
return new FakePersoneRepository(ListPersone);
}
[TestMethod]
public void ListaPersoneInCittaCorretto()
{
PersoneController p = new PersoneController(getPersoneRepository());
var j = p.PersoneRepository.ListPersoneDaCitta("Monza");
Assert.IsTrue(j.Count() == 1);
}
Comunque la Dependency Injection è solo una delle possibili sfide quando si creano unit test, dal momento che nelle reali applicazioni ci sono svariate dipendenze che occorre risolvere con oggetti 'finti'.
Nessun commento:
Posta un commento