web-dev-qa-db-de.com

Verspottung von EF DbContext mit Moq

Ich versuche, einen Komponententest für meinen Dienst mit einem verspotteten DbContext zu erstellen. Ich habe ein Interface IDbContext mit den folgenden Funktionen erstellt:

public interface IDbContext : IDisposable
{
    IDbSet<T> Set<T>() where T : class;
    DbEntityEntry<T> Entry<T>(T entity) where T : class;
    int SaveChanges();
}

Mein realer Kontext implementiert diese Schnittstelle IDbContext und DbContext.

Jetzt versuche ich, den IDbSet<T> im Kontext zu verspotten, so dass stattdessen ein List<User> zurückgegeben wird.

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

Ich erhalte diese Fehlermeldung immer bei .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

Ich konnte es lösen, indem ich eine FakeDbSet<T>-Klasse erstellte, die IDbSet<T> implementiert.

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

Nun sieht mein Test so aus:

[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

Danke Gaui für deine tolle Idee =)

Ich habe Ihrer Lösung einige Verbesserungen hinzugefügt und möchte sie weitergeben.

  1. Meine FakeDbSet erbt auch von DbSet, um zusätzliche Methoden zu erhalten wie AddRange()
  2. Ich habe den ObservableCollection<T> durch List<T> ersetzt, um alle __. Bereits implementierten Methoden in List<> bis zu meiner FakeDbSet zu übergeben

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

Es ist sehr einfach, das dbSet zu ändern und das EF-Kontextobjekt zu simulieren:

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

Jetzt können Linq-Abfragen ausgeführt werden. Beachten Sie jedoch, dass Fremdschlüsselverweise möglicherweise nicht automatisch erstellt werden:

    var user = contextMock.Object.Users.SingeOrDefault(userItem => userItem.Id == 42);

Da das Kontextobjekt verspottet wird, führt Context.SaveChanges() nichts aus, und Eigenschaftsänderungen Ihrer Entites werden möglicherweise nicht in Ihr dbSet eingefügt. Ich löste dies, indem ich meine SetModifed()-Methode verspottete, um die Änderungen aufzufüllen.

12
szuuuken

Falls noch jemand interessiert ist, hatte ich das gleiche Problem und fand diesen Artikel sehr hilfreich: Entity Framework Testing mit einem Mocking Framework (ab EF6)

Sie gilt nur für Entity Framework 6 oder neuer, deckt jedoch alles ab, von einfachen SaveChanges-Tests bis hin zu asynchronen Abfrageprüfungen, die alle mit Moq (und einigen manuellen Klassen) arbeiten.

9
eitamal

Wenn noch jemand nach Antworten sucht, habe ich eine kleine Bibliothek implementiert, um DbContext zu verspotten.

schritt 1

Installieren Sie das Coderful.EntityFramework.Testing nuget-Paket:

Install-Package Coderful.EntityFramework.Testing

schritt 2

Dann erstellen Sie eine Klasse wie folgt:

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

schritt 3

Jetzt können Sie sehr einfach Mocks erstellen:

// 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;

Und dann benutze deinen Spott:

// 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();

Vollständiger Artikel: http://www.22bugs.co/post/Mocking-DbContext/

4
niaher

Basierend auf diesem MSDN - Artikel habe ich meine eigenen Bibliotheken erstellt, um DbContext und DbSet zu verspotten:

  • EntityFrameworkMock - GitHub
  • EntityFrameworkMockCore - GitHub

Beide sind auf NuGet und GitHub verfügbar.

Der Grund, warum ich diese Bibliotheken erstellt habe, ist, dass ich das Verhalten SaveChanges emulieren wollte, beim Einfügen von Modellen mit demselben Primärschlüssel eine DbUpdateException werfen und in den Modellen Primärschlüssel mit mehreren Spalten/automatischem Inkrement unterstützen soll.

Da DbSetMock und DbContextMock von Mock<DbSet> und Mock<DbContext erben, können Sie außerdem alle Funktionen des Moq-Frameworks verwenden.

Neben Moq gibt es auch eine NSubstitute-Implementierung.

Die Verwendung mit der Moq-Version sieht folgendermaßen aus:

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

Ich bin spät dran, fand diesen Artikel jedoch hilfreich: Testen mit InMemory (MSDN Docs).

Es wird erläutert, wie Sie einen In-Memory-DB-Kontext (der keine Datenbank ist) mit dem Vorteil einer sehr geringen Codierung und der Möglichkeit zum Testen Ihrer DBContext-Implementierung verwenden.

0