Questo è un argomento che mi interessa molto. Ci sono molti puristi che affermano che non dovresti testare tecnologie come EF e NHibernate. Hanno ragione, sono già sottoposti a test molto rigorosi e, come indicato in una risposta precedente, è spesso inutile dedicare una grande quantità di tempo a testare ciò che non si possiede.
Tuttavia, possiedi il database sottostante! È qui che secondo me questo approccio si interrompe, non è necessario verificare che EF / NH svolgano correttamente il proprio lavoro. È necessario verificare che le mappature / implementazioni funzionino con il database. Secondo me questa è una delle parti più importanti di un sistema che puoi testare.
A rigor di termini, tuttavia, ci stiamo spostando dal dominio dei test unitari ai test di integrazione, ma i principi rimangono gli stessi.
La prima cosa che devi fare è essere in grado di deridere il tuo DAL in modo che il tuo BLL possa essere testato indipendentemente da EF e SQL. Questi sono i test unitari. Successivamente devi progettare i tuoi test di integrazione per provare il tuo DAL, a mio avviso questi sono altrettanto importanti.
Ci sono un paio di cose da considerare:
- Il database deve essere in uno stato noto ad ogni test. La maggior parte dei sistemi utilizza un backup o crea script per questo.
- Ogni test deve essere ripetibile
- Ogni test deve essere atomico
Esistono due approcci principali per la configurazione del database, il primo è quello di eseguire uno script DB di creazione UnitTest. Ciò garantisce che il database di unit test sia sempre nello stesso stato all'inizio di ogni test (è possibile reimpostare questo o eseguire ciascun test in una transazione per garantire ciò).
L'altra opzione è quella che faccio, eseguo impostazioni specifiche per ogni singolo test. Credo che questo sia l'approccio migliore per due motivi principali:
- Il tuo database è più semplice, non è necessario un intero schema per ogni test
- Ogni test è più sicuro, se modifichi un valore nel tuo script di creazione non invalida decine di altri test.
Purtroppo il tuo compromesso qui è la velocità. Ci vuole tempo per eseguire tutti questi test, per eseguire tutti questi script di installazione / smontaggio.
Un ultimo punto, può essere molto difficile scrivere una quantità così grande di SQL per testare il tuo ORM. È qui che adotto un approccio molto brutto (i puristi qui non saranno d'accordo con me). Uso il mio ORM per creare il mio test! Invece di avere uno script separato per ogni test DAL nel mio sistema, ho una fase di configurazione del test che crea gli oggetti, li collega al contesto e li salva. Eseguo quindi il mio test.
Questa è lungi dall'essere la soluzione ideale, ma in pratica trovo che sia MOLTO più facile da gestire (specialmente quando hai diverse migliaia di test), altrimenti stai creando un numero enorme di script. Praticità sulla purezza.
Senza dubbio guarderò indietro a questa risposta tra qualche anno (mesi / giorni) e non sarò d'accordo con me stesso poiché i miei approcci sono cambiati, tuttavia questo è il mio approccio attuale.
Per provare a riassumere tutto ciò che ho detto sopra questo è il mio tipico test di integrazione DB:
[Test]
public void LoadUser()
{
this.RunTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
return user.UserID;
}, id => // the ID of the entity we need to load
{
var user = LoadMyUser(id); // load the entity
Assert.AreEqual("Mr", user.Title); // test your properties
Assert.AreEqual("Joe", user.Firstname);
Assert.AreEqual("Bloggs", user.Lastname);
}
}
La cosa fondamentale da notare qui è che le sessioni dei due loop sono completamente indipendenti. Nell'implementazione di RunTest è necessario assicurarsi che il contesto sia sottoposto a commit e distrutto e che i dati possano provenire dal database solo per la seconda parte.
Modifica 13/10/2014
Ho detto che probabilmente avrei rivisto questo modello nei prossimi mesi. Mentre sostengo ampiamente l'approccio che ho sostenuto sopra, ho leggermente aggiornato il mio meccanismo di test. Ora tendo a creare le entità in TestSetup e TestTearDown.
[SetUp]
public void Setup()
{
this.SetupTest(session => // the NH/EF session to attach the objects to
{
var user = new UserAccount("Mr", "Joe", "Bloggs");
session.Save(user);
this.UserID = user.UserID;
});
}
[TearDown]
public void TearDown()
{
this.TearDownDatabase();
}
Quindi testare ogni proprietà singolarmente
[Test]
public void TestTitle()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Mr", user.Title);
}
[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}
[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Bloggs", user.Lastname);
}
Esistono diversi motivi per questo approccio:
- Non ci sono chiamate al database aggiuntive (una configurazione, uno smontaggio)
- I test sono molto più granulari, ogni test verifica una proprietà
- La logica di Setup / TearDown viene rimossa dai metodi di test stessi
Ritengo che ciò renda la classe di test più semplice e i test più granulari (i singoli assert sono buoni )
Modifica il 03/05/2015
Un'altra revisione su questo approccio. Mentre le configurazioni a livello di classe sono molto utili per test come il caricamento delle proprietà, sono meno utili laddove sono richieste le diverse configurazioni. In questo caso, l'impostazione di una nuova classe per ciascun caso è eccessiva.
Per aiutare con questo, ora tendo ad avere due classi base SetupPerTest
e SingleSetup
. Queste due classi espongono il framework come richiesto.
Nel SingleSetup
abbiamo un meccanismo molto simile come descritto nella mia prima modifica. Un esempio sarebbe
public TestProperties : SingleSetup
{
public int UserID {get;set;}
public override DoSetup(ISession session)
{
var user = new User("Joe", "Bloggs");
session.Save(user);
this.UserID = user.UserID;
}
[Test]
public void TestLastname()
{
var user = LoadMyUser(this.UserID); // load the entity
Assert.AreEqual("Bloggs", user.Lastname);
}
[Test]
public void TestFirstname()
{
var user = LoadMyUser(this.UserID);
Assert.AreEqual("Joe", user.Firstname);
}
}
Tuttavia, i riferimenti che assicurano il caricamento solo delle entità corrette possono utilizzare un approccio SetupPerTest
public TestProperties : SetupPerTest
{
[Test]
public void EnsureCorrectReferenceIsLoaded()
{
int friendID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriend();
session.Save(user);
friendID = user.Friends.Single().FriendID;
} () =>
{
var user = GetUser();
Assert.AreEqual(friendID, user.Friends.Single().FriendID);
});
}
[Test]
public void EnsureOnlyCorrectFriendsAreLoaded()
{
int userID = 0;
this.RunTest(session =>
{
var user = CreateUserWithFriends(2);
var user2 = CreateUserWithFriends(5);
session.Save(user);
session.Save(user2);
userID = user.UserID;
} () =>
{
var user = GetUser(userID);
Assert.AreEqual(2, user.Friends.Count());
});
}
}
In sintesi, entrambi gli approcci funzionano a seconda di ciò che si sta tentando di testare.