J'essaie de créer un test unitaire pour mon service avec un DbContext simulé. J'ai créé une interface IDbContext
avec les fonctions suivantes:
public interface IDbContext : IDisposable
{
IDbSet<T> Set<T>() where T : class;
DbEntityEntry<T> Entry<T>(T entity) where T : class;
int SaveChanges();
}
Mon contexte réel implémente cette interface IDbContext
et DbContext
.
Maintenant, j'essaie de simuler le IDbSet<T>
dans le contexte pour qu'il retourne un List<User>
à la place.
[TestMethod]
public void TestGetAllUsers()
{
// Arrange
var mock = new Mock<IDbContext>();
mock.Setup(x => x.Set<User>())
.Returns(new List<User>
{
new User { ID = 1 }
});
UserService userService = new UserService(mock.Object);
// Act
var allUsers = userService.GetAllUsers();
// Assert
Assert.AreEqual(1, allUsers.Count());
}
J'ai toujours cette erreur sur .Returns
:
The best overloaded method match for
'Moq.Language.IReturns<AuthAPI.Repositories.IDbContext,System.Data.Entity.IDbSet<AuthAPI.Models.Entities.User>>.Returns(System.Func<System.Data.Entity.IDbSet<AuthAPI.Models.Entities.User>>)'
has some invalid arguments
J'ai réussi à le résoudre en créant une classe FakeDbSet<T>
qui implémente IDbSet<T>
public class FakeDbSet<T> : IDbSet<T> where T : class
{
ObservableCollection<T> _data;
IQueryable _query;
public FakeDbSet()
{
_data = new ObservableCollection<T>();
_query = _data.AsQueryable();
}
public virtual T Find(params object[] keyValues)
{
throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
}
public T Add(T item)
{
_data.Add(item);
return item;
}
public T Remove(T item)
{
_data.Remove(item);
return item;
}
public T Attach(T item)
{
_data.Add(item);
return item;
}
public T Detach(T item)
{
_data.Remove(item);
return item;
}
public T Create()
{
return Activator.CreateInstance<T>();
}
public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
{
return Activator.CreateInstance<TDerivedEntity>();
}
public ObservableCollection<T> Local
{
get { return _data; }
}
Type IQueryable.ElementType
{
get { return _query.ElementType; }
}
System.Linq.Expressions.Expression IQueryable.Expression
{
get { return _query.Expression; }
}
IQueryProvider IQueryable.Provider
{
get { return _query.Provider; }
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return _data.GetEnumerator();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return _data.GetEnumerator();
}
}
Maintenant, mon test ressemble à ceci:
[TestMethod]
public void TestGetAllUsers()
{
//Arrange
var mock = new Mock<IDbContext>();
mock.Setup(x => x.Set<User>())
.Returns(new FakeDbSet<User>
{
new User { ID = 1 }
});
UserService userService = new UserService(mock.Object);
// Act
var allUsers = userService.GetAllUsers();
// Assert
Assert.AreEqual(1, allUsers.Count());
}
Merci Gaui pour ta super idée =)
J'ai ajouté quelques améliorations à votre solution et je souhaite la partager.
FakeDbSet
hérite également de DbSet
pour obtenir des méthodes supplémentaires like AddRange()
ObservableCollection<T>
par le List<T>
pour transmettre toutes les méthodes déjà implémentées dans List<>
jusqu'à ma FakeDbSet
.Mon FakeDbSet:
public class FakeDbSet<T> : DbSet<T>, IDbSet<T> where T : class {
List<T> _data;
public FakeDbSet() {
_data = new List<T>();
}
public override T Find(params object[] keyValues) {
throw new NotImplementedException("Derive from FakeDbSet<T> and override Find");
}
public override T Add(T item) {
_data.Add(item);
return item;
}
public override T Remove(T item) {
_data.Remove(item);
return item;
}
public override T Attach(T item) {
return null;
}
public T Detach(T item) {
_data.Remove(item);
return item;
}
public override T Create() {
return Activator.CreateInstance<T>();
}
public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T {
return Activator.CreateInstance<TDerivedEntity>();
}
public List<T> Local {
get { return _data; }
}
public override IEnumerable<T> AddRange(IEnumerable<T> entities) {
_data.AddRange(entities);
return _data;
}
public override IEnumerable<T> RemoveRange(IEnumerable<T> entities) {
for (int i = entities.Count() - 1; i >= 0; i--) {
T entity = entities.ElementAt(i);
if (_data.Contains(entity)) {
Remove(entity);
}
}
return this;
}
Type IQueryable.ElementType {
get { return _data.AsQueryable().ElementType; }
}
Expression IQueryable.Expression {
get { return _data.AsQueryable().Expression; }
}
IQueryProvider IQueryable.Provider {
get { return _data.AsQueryable().Provider; }
}
IEnumerator IEnumerable.GetEnumerator() {
return _data.GetEnumerator();
}
IEnumerator<T> IEnumerable<T>.GetEnumerator() {
return _data.GetEnumerator();
}
}
Il est très facile de modifier le dbSet et de simuler l'objet de contexte EF:
var userDbSet = new FakeDbSet<User>();
userDbSet.Add(new User());
userDbSet.Add(new User());
var contextMock = new Mock<MySuperCoolDbContext>();
contextMock.Setup(dbContext => dbContext.Users).Returns(userDbSet);
Il est maintenant possible d'exécuter des requêtes Linq, mais sachez que les références de clé étrangère ne peuvent pas être créées automatiquement:
var user = contextMock.Object.Users.SingeOrDefault(userItem => userItem.Id == 42);
Puisque l'objet contextuel est simulé, la fonction Context.SaveChanges()
ne fera rien et les modifications de propriétés de vos entités risquent de ne pas être renseignées dans votre dbSet. J'ai résolu ce problème en me moquant de ma méthode SetModifed()
pour renseigner les modifications.
Si le problème persiste, j’ai le même problème et je trouve cet article très utile: Test du framework d’entité avec un framework moqueur (à partir de EF6)
Cela ne s'applique qu'à Entity Framework 6 ou plus récent, mais il couvre tout, des simples tests SaveChanges aux tests asynchrones, tous utilisant Moq (et quelques classes manuelles).
Si quelqu'un cherche toujours des réponses, j'ai mis en place une petite bibliothèque pour permettre de se moquer de DbContext.
Installez le package de nuget Coderful.EntityFramework.Testing :
Install-Package Coderful.EntityFramework.Testing
Puis créez une classe comme celle-ci:
internal static class MyMoqUtilities
{
public static MockedDbContext<MyDbContext> MockDbContext(
IList<Contract> contracts = null,
IList<User> users = null)
{
var mockContext = new Mock<MyDbContext>();
// Create the DbSet objects.
var dbSets = new object[]
{
MoqUtilities.MockDbSet(contracts, (objects, contract) => contract.ContractId == (int)objects[0] && contract.AmendmentId == (int)objects[1]),
MoqUtilities.MockDbSet(users, (objects, user) => user.Id == (int)objects[0])
};
return new MockedDbContext<SourcingDbContext>(mockContext, dbSets);
}
}
Maintenant, vous pouvez créer des simulations super facilement:
// Create test data.
var contracts = new List<Contract>
{
new Contract("#1"),
new Contract("#2")
};
var users = new List<User>
{
new User("John"),
new User("Jane")
};
// Create DbContext with the predefined test data.
var dbContext = MyMoqUtilities.MockDbContext(
contracts: contracts,
users: users).DbContext.Object;
Et utilisez ensuite votre maquette:
// Create.
var newUser = dbContext.Users.Create();
// Add.
dbContext.Users.Add(newUser);
// Remove.
dbContext.Users.Remove(someUser);
// Query.
var john = dbContext.Users.Where(u => u.Name == "John");
// Save changes won't actually do anything, since all the data is kept in memory.
// This should be ideal for unit-testing purposes.
dbContext.SaveChanges();
Article complet: http://www.22bugs.co/post/Mocking-DbContext/
Sur la base de cet article MSDN , j'ai créé mes propres bibliothèques pour se moquer de DbContext
et DbSet
:
Tous deux disponibles sur NuGet et GitHub.
J'ai créé ces bibliothèques parce que je voulais émuler le comportement SaveChanges
, émettre un DbUpdateException
lors de l'insertion de modèles avec la même clé primaire et prendre en charge des clés primaires multi-colonnes/à incrémentation automatique dans les modèles.
De plus, étant donné que DbSetMock
et DbContextMock
héritent de Mock<DbSet>
et Mock<DbContext
, vous pouvez utiliser toutes les fonctionnalités du Moq framework .
À côté de Moq, il existe également une implémentation NSubstitute.
L'utilisation avec la version Moq ressemble à ceci:
public class User
{
[Key, Column(Order = 0)]
public Guid Id { get; set; }
public string FullName { get; set; }
}
public class TestDbContext : DbContext
{
public TestDbContext(string connectionString)
: base(connectionString)
{
}
public virtual DbSet<User> Users { get; set; }
}
[TestFixture]
public class MyTests
{
var initialEntities = new[]
{
new User { Id = Guid.NewGuid(), FullName = "Eric Cartoon" },
new User { Id = Guid.NewGuid(), FullName = "Billy Jewel" },
};
var dbContextMock = new DbContextMock<TestDbContext>("fake connectionstring");
var usersDbSetMock = dbContextMock.CreateDbSetMock(x => x.Users, initialEntities);
// Pass dbContextMock.Object to the class/method you want to test
// Query dbContextMock.Object.Users to see if certain users were added or removed
// or use Mock Verify functionality to verify if certain methods were called: usersDbSetMock.Verify(x => x.Add(...), Times.Once);
}
Je suis en retard, mais j'ai trouvé cet article utile: Test avec InMemory (MSDN Docs).
Il explique comment utiliser un contexte de base de données en mémoire (qui n'est pas une base de données) avec l'avantage de très peu de codage et la possibilité de tester votre implémentation DBContext
.