Сбой теста при попытке макета Entity Framework с помощью MOQ - PullRequest
0 голосов
/ 13 февраля 2019

Я пишу консольное приложение ASP.NET, чтобы попрактиковаться в создании Entity Framework с MOQ для тестирования.Приложение управляет книжным магазином и имеет базовый метод 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;
    }
}

Этот метод протестирован с использованием следующего метода тестирования:

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

Этот метод не выполняетсяследующая ошибка:

Сообщение: метод теста BookStoreNonCore.Tests.NonQueryTests.Test_EditPrice вызвал исключение:
System.NotImplementedException: член 'IQueryable.Provider' не был реализован для типа 'DbSet'1Proxy ', который наследуется от' DbSet`1 '.Двойные тесты для 'DbSet'1' должны обеспечивать реализацию используемых методов и свойств.

Вслед за отладчиком тест завершается ошибкой в ​​строке основного EditPrice метода

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

Я пока не совсем разбираюсь в фиктивном тестировании и не уверен, почему это не удается.Кто-нибудь сможет объяснить и предложить решение?

Ответы [ 3 ]

0 голосов
/ 13 февраля 2019

Я решил это, используя запрос Linq, а не элемент Single:

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

Затем следовал примеру на этом веб-сайте для тестирования сценариев запросов

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

, чтобы написать этот метод испытаний, который сейчас работает:

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

Спасибо вам обоим за то, что вы указали мне правильное направление (или хотя бы далеко от причины проблемы)).

0 голосов
/ 14 февраля 2019

Напоминаю, что при использовании Mock я столкнулся с проблемами при тестировании асинхронных операций EF.

Чтобы исправить это, вы можете извлечь интерфейс из вашего DbContext и создать второй "поддельный" DbContext.Этот Fake может содержать несколько классов FakeDbSet (наследующих DbSet).

Ознакомьтесь с этой документацией MS, более конкретно с разделом «Тестирование с асинхронными запросами»: https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

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

Класс FakeDbSet должен иметь несколько переопределений, чтобы возвращать эти различные реализации,также упомянуто в документации:

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

За исключением того, что вместо настройки в Mock, это просто переопределение метода в вашем собственном классе.

Преимущество этого в том, что вы можете установитьПоддельные данные в ваших модульных тестах более компактны и удобочитаемы, чем настройка ложных и поддельных возвратов.Например:

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

В EFCore все это не имеет значения, и вы можете просто использовать тип базы данных в памяти, конечно.

0 голосов
/ 13 февраля 2019

Из того, что я помню, макетирование структуры сущностей таким способом ОЧЕНЬ сложно, я полагаю, что если вы очень непреклонны в тестировании инфраструктуры таким образом, то, возможно, лучше обернуть ваш контекст в интерфейс IBookContext и получитьсобственные методы, обертывающие функциональность структуры сущностей, так что вещи легче переносить, и вам не нужно иметь дело с сущностью.

Оба находятся в реализациях памяти структуры сущностей - вы можете использовать их в тестах, чтобы выне нужно интегрировать с базой данных (что медленно)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...