C'est un sujet qui m'intéresse beaucoup. De nombreux puristes disent qu'il ne faut pas tester des technologies telles que EF et NHibernate. Ils ont raison, ils sont déjà testés très rigoureusement et comme une réponse précédente l'a indiqué, il est souvent inutile de passer beaucoup de temps à tester ce que vous ne possédez pas.
Cependant, vous possédez la base de données en dessous! C'est là que cette approche, à mon avis, échoue, vous n'avez pas besoin de tester que EF / NH fait correctement son travail. Vous devez tester que vos mappages / implémentations fonctionnent avec votre base de données. À mon avis, c'est l'une des parties les plus importantes d'un système que vous puissiez tester.
Cependant, à strictement parler, nous sortons du domaine des tests unitaires pour nous diriger vers les tests d'intégration, mais les principes restent les mêmes.
La première chose que vous devez faire est de pouvoir simuler votre DAL afin que votre BLL puisse être testé indépendamment d'EF et de SQL. Ce sont vos tests unitaires. Ensuite, vous devez concevoir vos tests d'intégration pour prouver votre DAL, à mon avis, ils sont tout aussi importants.
Il y a plusieurs choses à considérer:
- Votre base de données doit être dans un état connu à chaque test. La plupart des systèmes utilisent une sauvegarde ou créent des scripts pour cela.
- Chaque test doit être répétable
- Chaque test doit être atomique
Il existe deux approches principales pour configurer votre base de données, la première consiste à exécuter un script de création de base de données UnitTest. Cela garantit que votre base de données de tests unitaires sera toujours dans le même état au début de chaque test (vous pouvez le réinitialiser ou exécuter chaque test dans une transaction pour vous en assurer).
Votre autre option est ce que je fais, exécuter des configurations spécifiques pour chaque test individuel. Je pense que c'est la meilleure approche pour deux raisons principales:
- Votre base de données est plus simple, vous n'avez pas besoin d'un schéma complet pour chaque test
- Chaque test est plus sûr, si vous modifiez une valeur dans votre script de création, cela n'invalide pas des dizaines d'autres tests.
Malheureusement, votre compromis ici est la vitesse. Il faut du temps pour exécuter tous ces tests, pour exécuter tous ces scripts de configuration / suppression.
Un dernier point, il peut être très difficile d'écrire une si grande quantité de SQL pour tester votre ORM. C'est là que j'adopte une approche très méchante (les puristes ici ne seront pas d'accord avec moi). J'utilise mon ORM pour créer mon test! Plutôt que d'avoir un script séparé pour chaque test DAL dans mon système, j'ai une phase de configuration de test qui crée les objets, les attache au contexte et les enregistre. Je lance ensuite mon test.
C'est loin d'être la solution idéale, mais en pratique, je trouve que c'est BEAUCOUP plus facile à gérer (surtout lorsque vous avez plusieurs milliers de tests), sinon vous créez un nombre énorme de scripts. Pratique plutôt que pureté.
Je vais sans aucun doute revenir sur cette réponse dans quelques années (mois / jours) et ne pas être d'accord avec moi-même car mes approches ont changé - mais c'est mon approche actuelle.
Pour essayer de résumer tout ce que j'ai dit ci-dessus, voici mon test d'intégration DB typique:
[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);
}
}
L'essentiel à noter ici est que les sessions des deux boucles sont totalement indépendantes. Dans votre implémentation de RunTest vous devez vous assurer que le contexte est validé et détruit et que vos données ne peuvent provenir de votre base de données que pour la deuxième partie.
Modifier le 13/10/2014
J'ai dit que je réviserais probablement ce modèle au cours des prochains mois. Bien que je maintienne largement l'approche que j'ai préconisée ci-dessus, j'ai légèrement mis à jour mon mécanisme de test. J'ai maintenant tendance à créer les entités dans TestSetup et 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();
}
Ensuite, testez chaque propriété individuellement
[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);
}
Il y a plusieurs raisons à cette approche:
- Il n'y a pas d'appels de base de données supplémentaires (une configuration, un démontage)
- Les tests sont beaucoup plus granulaires, chaque test vérifie une propriété
- La logique Setup / TearDown est supprimée des méthodes de test elles-mêmes
Je pense que cela rend la classe de test plus simple et les tests plus granulaires (les assertions simples sont bonnes )
Modifier le 03/05/2015
Une autre révision de cette approche. Bien que les configurations au niveau de la classe soient très utiles pour les tests tels que le chargement des propriétés, elles le sont moins lorsque les différentes configurations sont requises. Dans ce cas, la création d'une nouvelle classe pour chaque cas est excessive.
Pour vous aider, j'ai maintenant tendance à avoir deux classes de base SetupPerTest
et SingleSetup
. Ces deux classes exposent le framework selon les besoins.
Dans le SingleSetup
nous avons un mécanisme très similaire à celui décrit dans ma première modification. Un exemple serait
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);
}
}
Cependant, les références qui garantissent que seules les entités correctes sont chargées peuvent utiliser une approche 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());
});
}
}
En résumé, les deux approches fonctionnent en fonction de ce que vous essayez de tester.