web-dev-qa-db-fra.com

Moqueur EF DbContext avec Moq

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
44
Gaui

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());
}
27
Gaui

Merci Gaui pour ta super idée =)

J'ai ajouté quelques améliorations à votre solution et je souhaite la partager.

  1. Ma FakeDbSet hérite également de DbSet pour obtenir des méthodes supplémentaires like AddRange()
  2. J'ai remplacé le 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.

12
szuuuken

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).

9
eitamal

Si quelqu'un cherche toujours des réponses, j'ai mis en place une petite bibliothèque pour permettre de se moquer de DbContext.

étape 1

Installez le package de nuget Coderful.EntityFramework.Testing :

Install-Package Coderful.EntityFramework.Testing

étape 2

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); 
    }
}

étape 3

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/

4
niaher

Sur la base de cet article MSDN , j'ai créé mes propres bibliothèques pour se moquer de DbContext et DbSet:

  • EntityFrameworkMock - GitHub
  • EntityFrameworkMockCore - GitHub

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);
}
2
huysentruitw

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.

0
Quality Catalyst