J'ai récemment posé la question suivante sur la mailling list française d'alt.net :
J'aimerai savoir comment vous testez vos repository dans vos projets,
et quels sont les + et les - de vos solutions.
Je sais qu'il y a plusieurs solutions :
1) Tester le repository avec une bdd de dev "classique"
2) Tester le repository avec une base sqlite chargée en mémoire
3) Ajouter une couche d'abstraction supplémentaire entre le repository
et la persistance, puis remplacer la couche persistance lors des tests
par une implémentation Fake à base de hastable ou autre.
La solution 1) n'est pas une option à cause de la vitesse d'execution
des tests.
La solution 2) est très intéressante mais un peu galère à mettre en
place (car il faut recréer le schéma systématiquement, sachant qu'un
schéma pour sql server n'est pas compatible avec sqlite sans
modifications manuelles). Je voulais utiliser migrator.net pour gérer
le problème mais c'est un peu buggé.
La solution 3) me parait la plus simple, mais elle ajoute un niveau
d'abstraction supplémentaire qui n'a à priori raison d'être que pour
les tests
Quel est votre expérience? Que pensez vous de chaque solution? quelles
sont vos best-practices?
Thanks!
J'ai eu des réponses très intéressantes à cette question qui m'ont permis de réfléchir et de trouver la méthode qui me satisfait le plus.
Pour comprendre mon choix, il faut d'abord revenir sur le pattern Repository...
La façon la plus simple d'implémenter ce pattern est d'utiliser un repository générique dont hériteront des repository typés. On se retrouve par exemple avec cela :
public interface IRepository where T : Entity
{
T Find(long id);
T FindBy(Expression> where);
IList Fetch(Expression> where);
IList FetchAll();
void Save(T target);
void Delete(T target);
}
public interface IProductRepository : IRepository
{
IList FetchTopProducts();
IPaginable Search(string keywords, long categoryId);
}
Puis :
public class Repository : IRepository
{
// ...
}
public class ProductRepository : Repository, IProductRepository
{
// ...
}
Autrement dit, Repository se charge des fonctions de bases : chercher par id, sauvegarder, supprimer... Et ProductRepository se charge de construire les requêtes spécifiques a l'entité Product. C'est simple, rapide, efficace!
Cependant, cette implémentation à deux inconvénients :
- Repository est une simple sur-couche à l'ORM que l'on utilise (NHibernate, Linq to Sql, Entity Framework, etc.). Pour tester ses repository, on est donc obliger de taper dans une base de test (ce qui est généralement lent) ou de mocker l'ORM (ce qui rends l'écriture des tests unitaires très fastidieuse).
- Systématiquement hériter de Repository rends accessible l'ensemble des fonctionnalités de cette même classe. Hors cela peut être problématique du point de vue du domaine si l'on veut par exemple interdire de supprimer une entité! Pourquoi devrait-on offrir la fonction dans l'interface si le comportement est à proscrire?
Une solution plus flexible consiste à injecter Repository dans ProductRepository. Soit :
public interface IProductRepository
{
Product Find(long id);
void Save(Product target);
IList<Product> FetchTopProducts();
IPaginable<Product> Search(string keywords, long categoryId);
}
public class ProductRepository : IProductRepository
{
private readonly IGenericRepository<Product> _genericRepository;
public ProductRepository(IGenericRepository<Product> genericRepository)
{
_genericRepository = genericRepository;
}
public Product Find(long id)
{
return _genericRepository.Find(id);
}
// ...
}
On constate alors que IProductRepository n'implémente plus IRepository, tout comme ProductRepository n'hérite plus de Repository.
Cette implémentation règle nos deux problèmes :
- Je n'expose que les fonctionnalités qui ont du sens par rapport à mon domaine. La qualité de ce dernier s'améliore en conséquence!
- Je peux injecter dans ProductRepository un InMemoryRepository pour faire mes tests. Je peux donc tester FetchTopProducts() sans faire d'aller retour sur la base et sans mocker mon ORM. Soit par exemple :
[Test]
public void should_return_top_products()
{
var memoryRepository =
new InMemoryRepository<Product>
();
var repository =
new ProductRepository
(memoryRepository
);
memoryRepository.
Save(new Product
("Product1",
true));
memoryRepository.
Save(new Product
("Product2",
true));
memoryRepository.
Save(new Product
("Product3",
false));
var results = _repository.FetchTopProducts();
results.Count.ShouldEqual(2);
results[0].Name.ShouldEqual("Product1");
results[0].IsTopProduct.ShouldBeTrue();
}