Verspotten von EF DbContext mit Moq

73

Ich versuche, einen Komponententest für meinen Dienst mit einem verspotteten DbContext zu erstellen. Ich habe eine Schnittstelle IDbContextmit 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 IDbContextund DbContext.

Jetzt versuche ich, das IDbSet<T>im Kontext zu verspotten , also gibt es List<User>stattdessen ein zurück.

[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 bekomme immer diesen Fehler auf .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
Gaui
quelle
2
Obwohl dieser Beitrag nützlich sein wird, denke ich, wäre es mehr, wenn Sie die Implementierung des Moq DbContext einbeziehen würden, danke für die Idee.
LostNomad311

Antworten:

35

Ich habe es geschafft, es zu lösen, indem ich eine FakeDbSet<T>Klasse erstellt habe, die implementiertIDbSet<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();
    }
}

Jetzt 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());
}
Gaui
quelle
Ich denke , es ist gut @Ladislav mrnk Kommentare hier zu berücksichtigen stackoverflow.com/questions/6904139/... und stackoverflow.com/questions/6766478/unit-testing-dbcontext
user8128167
Der Versuch, dies unter .net Core 1.0 zu implementieren, aber ein großer Schlüssel ist, dass IDbSet entfernt wurde und der Konstruktor privat ist, sodass ich nicht einmal meine eigene Schnittstelle extrahieren kann.
Paul Gorbas
Dies ist in EF6 nicht mehr die bevorzugte Methode, da EF6 neue Änderungen an DbSet hinzugefügt hat, die nicht im IDbSet enthalten sind (und wie oben in Core entfernt wurden). entityframework.codeplex.com/… DbSet ist stattdessen spöttischer, obwohl ich noch nicht sicher bin, wie die richtige Implementierung aussieht.
IronSean
@PaulGorbas Ich fand es einfacher, einen DbContext mit SqlLite einzurichten als mit Moq in .net Core 2, z . B. gist.github.com/mikebridge/a1188728a28f0f53b06fed791031c89d .
Mikebridge
Für ähnliche Fälschungen für die async EF-Überladungen siehe hier
StuartLC
20

Danke Gaui für deine tolle Idee =)

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

  1. Mein FakeDbSeterbt auch von DbSetzusätzlichen Methoden wieAddRange()
  2. Ich ersetzte die ObservableCollection<T>mit List<T>alle passieren bereits implementierten Methoden in List<>bis zu meinemFakeDbSet

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 verspotten:

    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 ist es möglich, Linq-Abfragen auszuführen. Beachten Sie jedoch, dass Fremdschlüsselreferenzen möglicherweise nicht automatisch erstellt werden:

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

Da das Context.SaveChanges()Kontextobjekt verspottet ist, werden keine Änderungen vorgenommen, und Eigenschaftsänderungen Ihrer Entites werden möglicherweise nicht in Ihr dbSet eingetragen. Ich habe dies gelöst, indem ich meine SetModifed()Methode zum Auffüllen der Änderungen verspottet habe .

szuuuken
quelle
Der Versuch, dies unter .net Core 1.0 zu implementieren, aber ein großer Schlüssel ist, dass IDbSet entfernt wurde und der Konstruktor privat ist, sodass ich nicht einmal meine eigene Schnittstelle extrahieren kann.
Paul Gorbas
1
Diese Antwort löste mein Problem mit dem Verspotten der EF .Add () - Funktion in ASP.NET MVC 5. Obwohl ich weiß, dass sie nicht mit Core zusammenhängt, konnte ich genügend Informationen extrahieren, um eine vorhandene Klasse zu erben, die das DBSet mit DBSET, IDBSET verspottet und überschreiben Sie die Add-Methode. Vielen Dank!
CSharpMinor
MySuperCoolDbContext ist dies eine Concreate-Klasse?
Jaydeep Shil
MySuperCoolDbContext ist der Name Ihres DbContext
szuuuken
18

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

Es gilt nur für Entity Framework 6 oder höher, deckt jedoch alles ab, von einfachen SaveChanges-Tests bis hin zu asynchronen Abfragetests, die alle Moq (und einige manuelle Klassen) verwenden.

Eitamal
quelle
7

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

Schritt 1

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

Install-Package Coderful.EntityFramework.Testing

Schritt 2

Erstellen Sie dann 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 ganz 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 dein Mock:

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

niaher
quelle
1
Vermutlich warten Sie das Paket nicht, um mit dem .net-Kern zu arbeiten. Hier ist der Fehler, den ich beim Versuch bekommen habe, es zu installieren: Package Coderful.EntityFramework.Testing 1.5.1 ist nicht kompatibel mit netcoreapp1.0
Paul Gorbas
@PaulGorbas Sie haben Recht, die Bibliothek wird nicht für .net Core aktualisiert. Verklagen Sie es mit EF Core?
Niaher
1
Mein xUnit-Testprojekt zielt auf das Framework .NetCoreApp 1.0 alias EF 7 b4 ab. Sie haben die Namenskonvention geändert
Paul Gorbas
4

Basierend auf diesem MSDN- Artikel habe ich meine eigenen Bibliotheken zum Verspotten erstellt DbContextund DbSet:

  • EntityFrameworkMock - GitHub
  • EntityFrameworkMockCore - GitHub

Beide sind auf NuGet und GitHub verfügbar.

Der Grund, warum ich diese Bibliotheken erstellt habe, ist, dass ich das SaveChangesVerhalten emulieren , DbUpdateExceptionbeim Einfügen von Modellen mit demselben Primärschlüssel ein auslösen und mehrspaltige / automatisch inkrementierte Primärschlüssel in den Modellen unterstützen wollte.

Da darüber hinaus beide DbSetMockund DbContextMockvererben Mock<DbSet>und Mock<DbContextkönnen Sie alle Funktionen des verwenden Moq Rahmen .

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);
}
huysentruitw
quelle
funktioniert das mit UpdateFromQueryAsync()von zzProjects? entityframework-extensions.net/update-from-query
JobaDiniz
4

Ich bin spät dran , fand diesen Artikel aber 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, Ihre DBContextImplementierung tatsächlich zu testen , verwenden.

Qualitätskatalysator
quelle