2
votes

Échec du test en essayant de se moquer d'Entity Framework avec MOQ

J'écris une application console ASP.NET pour m'entraîner à se moquer d'Entity Framework avec MOQ à des fins de test. L'application gère une librairie et dispose d'une méthode de base EditPrice comme indiqué ci-dessous:

Book book = context.Books.Single(b => b.Id == id);

Cette méthode est testée avec la méthode de test suivante:

    [TestMethod]
    public void Test_EditPrice()
    {
        // Arrange
        var mockSet = new Mock<DbSet<Book>>();

        var mockContext = new Mock<BookContext>();
        mockContext.Setup(m => m.Books).Returns(mockSet.Object);

        var service = new BookStore(mockContext.Object);
        service.AddBook(1, "Wuthering Heights", "Emily Brontë", "Classics", 7.99, 5);

        // Act
        service.EditPrice(1, 5.99);

        // Assert
        mockSet.Verify(m => m.Add(It.IsAny<Book>()), Times.Once());
        mockContext.Verify(m => m.SaveChanges(), Times.Exactly(2));
    }

Cette méthode échoue et génère l'erreur suivante:

Message: La méthode de test BookStoreNonCore.Tests.NonQueryTests.Test_EditPrice a levé une exception:
System.NotImplementedException: Le membre 'IQueryable.Provider' n'a pas été implémenté sur le type 'DbSet'1Proxy' qui hérite de 'DbSet`1'. Les doubles de test pour 'DbSet'1' doivent fournir des implémentations des méthodes et des propriétés utilisées.

Suite au débogueur, le test échoue sur la ligne de la méthode principale EditPrice

public class BookStore
{
    private BookContext context;

    public BookStore(BookContext newContext)
    {
        context = newContext;
    }

    // Edit the price of a book in the store
    public Book EditPrice(int id, double newPrice)
    {
        Book book = context.Books.Single(b => b.Id == id);
        book.Price = newPrice;
        context.SaveChanges();
        return book;
    }
}

Je n'ai pas tout à fait maîtrisé les tests simulés et je ne sais pas pourquoi cela échoue. Quelqu'un pourrait-il expliquer et fournir une solution?


1 commentaires

Le message d'erreur est clair! vous n'avez pas fourni d'implémentation pour les membres DbSet comme Single, SingleOrDefault, Add, Remove etc. Je suis un peu occupé sinon j'aurais pu vous aider davantage. J'espère que certains vous aideront.


3 Réponses :


2
votes

D'après ce dont je me souviens, se moquer du cadre d'entité de cette manière est TRÈS difficile, je suggère que si vous êtes très catégorique sur le test du cadre de cette manière, il peut être préférable d'envelopper votre contexte dans une interface IBookContext et ayez vos propres méthodes encapsulant la fonctionnalité du framework d'entité afin que les choses soient plus facilement modifiables et que vous n'ayez pas à vous occuper du framework d'entité.

Les deux sont des implémentations en mémoire du framework d'entité - vous pouvez les utiliser dans les tests afin de ne pas avoir à intégrer à une base de données (qui est lente)


0 commentaires

1
votes

Je l'ai résolu en utilisant une requête Linq plutôt que le membre unique:

    [TestMethod]
    public void Test_EditPrice()
    {
        // Arrange
        var data = new List<Book>
        {
            new Book(1, "Wuthering Heights", "Emily Brontë", "Classics", 7.99, 5)
        }.AsQueryable();

        var mockSet = new Mock<DbSet<Book>>();
        mockSet.As<IQueryable<Book>>().Setup(m => m.Provider).Returns(data.Provider);
        mockSet.As<IQueryable<Book>>().Setup(m => m.Expression).Returns(data.Expression);
        mockSet.As<IQueryable<Book>>().Setup(m => m.ElementType).Returns(data.ElementType);
        mockSet.As<IQueryable<Book>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

        var mockContext = new Mock<BookContext>();
        mockContext.Setup(c => c.Books).Returns(mockSet.Object);

        // Act
        var service = new BookStore(mockContext.Object);
        var books = service.GetAllBooks();
        service.EditPrice(1, 5.99);

        // Assert
        Assert.AreEqual(data.Count(), books.Count);
        Assert.AreEqual("Wuthering Heights", books[0].Title);
        Assert.AreEqual(5.99, books[0].Price);
    }

Ensuite, j'ai suivi l'exemple sur ce site Web pour tester les scénarios de requête

https://docs.microsoft.com/en -gb / ef / ef6 / fundamentals / testing / mocking

pour écrire cette méthode de test qui fonctionne désormais:

    // Edit the price of a book in the store
    public void EditPrice(int id, double newPrice)
    {
        var query = from book in context.Books
                    where book.Id == id
                    select book;

        Book BookToEdit = query.ToList()[0];
        BookToEdit.Price = newPrice;
        context.SaveChanges();
    }

Merci à vous deux de m'avoir pointé dans la bonne direction (ou du moins loin de la cause du problème).


0 commentaires

1
votes

Je me souviens que lors de l'utilisation d'un Mock, j'ai rencontré des problèmes lors du test d'opérations EF asynchrones.

Pour corriger cela, vous pouvez distiller une interface de votre DbContext et créer un deuxième "Fake" DbContext. Ce Fake pourrait contenir un certain nombre de classes FakeDbSet (héritant de DbSet).

Consultez cette documentation MS, plus spécifiquement la partie "Test avec des requêtes asynchrones": https://docs.microsoft.com/en-us/ ef / ef6 / fundamentals / testing / mocking

[TestClass]
public class BookTest 
{
    private FakeBooksDbContext context;

    [TestInitialize]
    public void Init()
    {
        context = new FakeBooksDbContext();
    }

    [TestMethod]
    public void When_PriceIs10_Then_X()
    {
        // Arrange
        SetupFakeData(10);

        // Act

        // Assert
    }

    private void SetupFakeData(int price) 
    {
        context.Books.Add(new Book { Price = price });
    }
}

La classe FakeDbSet doit avoir quelques remplacements pour renvoyer ces différentes implémentations, également mentionnées dans la documentation:

var mockSet = new Mock<DbSet<Blog>>();
mockSet.As<IDbAsyncEnumerable<Blog>>()
    .Setup(m => m.GetAsyncEnumerator())
    .Returns(new TestDbAsyncEnumerator<Blog>(data.GetEnumerator()));

mockSet.As<IQueryable<Blog>>()
    .Setup(m => m.Provider)
    .Returns(new TestDbAsyncQueryProvider<Blog>(data.Provider));

mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

Sauf qu'au lieu de le configurer dans un Mock, c'est juste un remplacement de méthode dans votre propre classe.

L'avantage de ceci est que vous pouvez définir de fausses données dans vos tests unitaires d'une manière plus compacte et plus lisible que la configuration de simulations et de faux retours. Par exemple:

using System.Collections.Generic;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;

namespace TestingDemo
{
    internal class TestDbAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
    {
        private readonly IQueryProvider _inner;

        internal TestDbAsyncQueryProvider(IQueryProvider inner)
        {
            _inner = inner;
        }

        public IQueryable CreateQuery(Expression expression)
        {
            return new TestDbAsyncEnumerable<TEntity>(expression);
        }

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            return new TestDbAsyncEnumerable<TElement>(expression);
        }

        public object Execute(Expression expression)
        {
            return _inner.Execute(expression);
        }

        public TResult Execute<TResult>(Expression expression)
        {
            return _inner.Execute<TResult>(expression);
        }

        public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
        {
            return Task.FromResult(Execute(expression));
        }

        public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
        {
            return Task.FromResult(Execute<TResult>(expression));
        }
    }

    internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
    {
        public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
            : base(enumerable)
        { }

        public TestDbAsyncEnumerable(Expression expression)
            : base(expression)
        { }

        public IDbAsyncEnumerator<T> GetAsyncEnumerator()
        {
            return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
        }

        IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
        {
            return GetAsyncEnumerator();
        }

        IQueryProvider IQueryable.Provider
        {
            get { return new TestDbAsyncQueryProvider<T>(this); }
        }
    }

    internal class TestDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
    {
        private readonly IEnumerator<T> _inner;

        public TestDbAsyncEnumerator(IEnumerator<T> inner)
        {
            _inner = inner;
        }

        public void Dispose()
        {
            _inner.Dispose();
        }

        public Task<bool> MoveNextAsync(CancellationToken cancellationToken)
        {
            return Task.FromResult(_inner.MoveNext());
        }

        public T Current
        {
            get { return _inner.Current; }
        }

        object IDbAsyncEnumerator.Current
        {
            get { return Current; }
        }
    }
}

Dans EFCore, tout cela n'est pas pertinent et vous pouvez simplement utiliser un type de base de données en mémoire bien sûr.


2 commentaires

J'utilise ce modèle depuis un certain temps maintenant, mais après avoir mis à jour Moq de 4.7.1 à 4.14.5, il a cessé de fonctionner avec cette erreur "Le membre 'IQueryable.Provider' n'a pas été implémenté sur le type 'DbSet 1Proxy 'qui hérite de' DbSet 1 '. Les doubles de test pour' DbSet`1 'doivent fournir des implémentations des méthodes et des propriétés utilisées. ". Des idées?


Correction! C'est Castle.Core version 4.4.0 qui a créé le problème, la mise à jour vers 4.4.1 l'a résolu. :-)