Як знущатися над асинхронним сховищем за допомогою Entity Framework Core


78

Я намагаюся створити модульний тест для класу, який викликає асинхронне сховище. Я використовую ASP.NET Core і Entity Framework Core. Моє загальне сховище виглядає так.

public class EntityRepository<TEntity> : IEntityRepository<TEntity> where TEntity : class
{
    private readonly SaasDispatcherDbContext _dbContext;
    private readonly DbSet<TEntity> _dbSet;

    public EntityRepository(SaasDispatcherDbContext dbContext)
    {
        _dbContext = dbContext;
        _dbSet = dbContext.Set<TEntity>();
    }

    public virtual IQueryable<TEntity> GetAll()
    {
        return _dbSet;
    }

    public virtual async Task<TEntity> FindByIdAsync(int id)
    {
        return await _dbSet.FindAsync(id);
    }

    public virtual IQueryable<TEntity> FindBy(Expression<Func<TEntity, bool>> predicate)
    {
        return _dbSet.Where(predicate);
    }

    public virtual void Add(TEntity entity)
    {
        _dbSet.Add(entity);
    }
    public virtual void Delete(TEntity entity)
    {
        _dbSet.Remove(entity);
    }

    public virtual void Update(TEntity entity)
    {
        _dbContext.Entry(entity).State = EntityState.Modified;
    }

    public virtual async Task SaveChangesAsync()
    {
        await _dbContext.SaveChangesAsync();
    }
}

Тоді у мене є клас послуг, який викликає FindBy та FirstOrDefaultAsync на екземплярі сховища:

    public async Task<Uri> GetCompanyProductURLAsync(Guid externalCompanyID, string productCode, Guid loginToken)
    {            
        CompanyProductUrl companyProductUrl = await _Repository.FindBy(u => u.Company.ExternalCompanyID == externalCompanyID && u.Product.Code == productCode.Trim()).FirstOrDefaultAsync();

        if (companyProductUrl == null)
        {
            return null;
        }

        var builder = new UriBuilder(companyProductUrl.Url);
        builder.Query = $"-s{loginToken.ToString()}";

        return builder.Uri;
    }

Я намагаюся знущатися із виклику сховища в моєму тесті нижче:

    [Fact]
    public async Task GetCompanyProductURLAsync_ReturnsNullForInvalidCompanyProduct()
    {
        var companyProducts = Enumerable.Empty<CompanyProductUrl>().AsQueryable();

        var mockRepository = new Mock<IEntityRepository<CompanyProductUrl>>();
        mockRepository.Setup(r => r.FindBy(It.IsAny<Expression<Func<CompanyProductUrl, bool>>>())).Returns(companyProducts);

        var service = new CompanyProductService(mockRepository.Object);

        var result = await service.GetCompanyProductURLAsync(Guid.NewGuid(), "wot", Guid.NewGuid());

        Assert.Null(result);
    }

Однак, коли тест виконує виклик до сховища, я отримую таку помилку:

The provider for the source IQueryable doesn't implement IAsyncQueryProvider. Only providers that implement IEntityQueryProvider can be used for Entity Framework asynchronous operations.

Як я можу правильно знущатися зі сховища, щоб це працювало?


2
Можливо, це може допомогти msdn.microsoft.com/en-us/library/dn314429.aspx
Nkosi

Прочитайте розділ проTesting with async queries
Нкосі

Ви повинні знущатися як IQueryable<T>і IAsyncEnumerableAccessor<T>інтерфейси , а також
Dealdiane

Відповіді:


97

Дякую @Nkosi за вказівку мені на посилання із прикладом того, що я робив те саме у EF 6: https://msdn.microsoft.com/en-us/library/dn314429.aspx . Це не працювало точно так, як є з EF Core, але я зміг почати з нього та внести зміни, щоб налагодити його роботу. Нижче наведені тестові класи, які я створив для "знущання" над IAsyncQueryProvider:

internal class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider
{
    private readonly IQueryProvider _inner;

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

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

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

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

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

    public IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression)
    {
        return new TestAsyncEnumerable<TResult>(expression);
    }

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

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

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

    public IAsyncEnumerator<T> GetEnumerator()
    {
        return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    }

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

internal class TestAsyncEnumerator<T> : IAsyncEnumerator<T>
{
    private readonly IEnumerator<T> _inner;

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

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

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

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

І ось мій оновлений тестовий приклад, який використовує такі класи:

[Fact]
public async Task GetCompanyProductURLAsync_ReturnsNullForInvalidCompanyProduct()
{
    var companyProducts = Enumerable.Empty<CompanyProductUrl>().AsQueryable();

    var mockSet = new Mock<DbSet<CompanyProductUrl>>();

    mockSet.As<IAsyncEnumerable<CompanyProductUrl>>()
        .Setup(m => m.GetEnumerator())
        .Returns(new TestAsyncEnumerator<CompanyProductUrl>(companyProducts.GetEnumerator()));

    mockSet.As<IQueryable<CompanyProductUrl>>()
        .Setup(m => m.Provider)
        .Returns(new TestAsyncQueryProvider<CompanyProductUrl>(companyProducts.Provider));

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

    var contextOptions = new DbContextOptions<SaasDispatcherDbContext>();
    var mockContext = new Mock<SaasDispatcherDbContext>(contextOptions);
    mockContext.Setup(c => c.Set<CompanyProductUrl>()).Returns(mockSet.Object);

    var entityRepository = new EntityRepository<CompanyProductUrl>(mockContext.Object);

    var service = new CompanyProductService(entityRepository);

    var result = await service.GetCompanyProductURLAsync(Guid.NewGuid(), "wot", Guid.NewGuid());

    Assert.Null(result);
}

Радий, що ти врешті зрозумів це. Я копався у вихідному коді на github, щоб перевірити, чи є у них якісь приклади moq. Досить смішно, я розслідував одного, коли перевірив і побачив, що ти прийшов до власного рішення. Класно.
Нкосі

Я б перетворив це на метод розширення, щоб ви могли використовувати його повторно у своїх тестах.
Нкосі

2
Перевірте дану тут відповідь, яка посилається на цю відповідь, де застосовувався метод розширення. Щасливого кодування !!!
Nkosi

Ще раз спасибі, @Nkosi!
Jed Veatch

6
Чи хотіли б ви оновити це до ядра 3.0? Я не можу пройти "Вираз аргументу не дійсний" на _inner.Execute <TResults> (вираз)
BillRob

44

Спробуйте використати моє розширення Moq / NSubstitute / FakeItEasy MockQueryable : підтримуються всі операції синхронізації / асинхронізації (див. Інші приклади тут )

//1 - create a List<T> with test items
var users = new List<UserEntity>()
{
 new UserEntity,
 ...
};

//2 - build mock by extension
var mock = users.AsQueryable().BuildMock();

//3 - setup the mock as Queryable for Moq
_userRepository.Setup(x => x.GetQueryable()).Returns(mock.Object);

//3 - setup the mock as Queryable for NSubstitute
_userRepository.GetQueryable().Returns(mock);

DbSet також підтримується

//2 - build mock by extension
var mock = users.AsQueryable().BuildMockDbSet();

//3 - setup DbSet for Moq
var userRepository = new TestDbSetRepository(mock.Object);

//3 - setup DbSet for NSubstitute
var userRepository = new TestDbSetRepository(mock);

Примітки:

  • AutoMapper також підтримується з версії 1.0.4
  • DbQuery підтримується з версії 1.1.0
  • EF Core 3.0 підтримується з версії 3.0.0
  • .Net 5 підтримується з версії 5.0.0

7
Це, здається, дуже приємний пакет для використання замість того, щоб писати купу подвійних реалізацій для підробки IAsyncQueryProviderтощо
infl3x

1
Це чудовий пакет, який відразу зробив свою справу. Дякую!
Sigge

1
@ Noob спробуйте відкрити посилання з мого коментаря, або просто подивіться github.com/romantitov/MockQueryable/blob/master/src/…
Р.Тітов

1
@Noob в MyService.CreateUserIfNotExist Я використовую FirstOrDefaultAsync, і тут ви можете побачити тести на логіку CreateUserIfNotExist github.com/romantitov/MockQueryable/blob/master/src/…
Р.Тітов,

1
@ R.Titov дякую, я знайшов свою помилку, по-перше, я не знущався зі своїх даних, які я мав повернути, а по-друге, створивши макет всередині return (), дав також якусь іншу помилку. У будь-якому разі, зараз це працює, дякую !!
Noob

8

Значно менше рішення коду. Використовуйте контекст db в пам'яті, який повинен подбати про завантаження всіх наборів для вас. Вам більше не потрібно висміювати DbSet у вашому контексті, але якщо ви хочете повернути дані із служби, наприклад, ви можете просто повернути фактично встановлені дані контексту в пам'яті.

DbContextOptions< SaasDispatcherDbContext > options = new DbContextOptionsBuilder< SaasDispatcherDbContext >()
  .UseInMemoryDatabase(Guid.NewGuid().ToString())
  .Options;

  _db = new SaasDispatcherDbContext(optionsBuilder: options);

Це здається хорошим рішенням. Чому ні? Додайте посилання на пакет nuget в пам'яті, щоб отримати метод розширення UseInMemoryDatabase. docs.microsoft.com/en-us/ef/core/miscellaneous/testing/…
andrew

Чи не буде це формою інтеграційного тесту? Я знаю, що це база даних у пам’яті, але ви будете тестувати проти системи, що є тестами інтеграції. Виправте мене, якщо я помиляюся.
elfico

2

Я підтримую два проекти з відкритим кодом, які важко піднімають налаштування глузувань і насправді наслідують SaveChanges(Async).

Для EF Core: https://github.com/huysentruitw/entity-framework-core-mock

Для EF6: https://github.com/huysentruitw/entity-framework-mock

Обидва проекти мають пакети Nuget з інтеграцією для Moq або NSubstitute.


2

Ось порт прийнятої відповіді на F #, я просто зробив це для себе і думав, що це може заощадити комусь час. Я також оновив приклад, щоб відповідати оновленому API C # 8 IAsyncEnumarable, і налаштував Mock-налаштування як загальний.

    type TestAsyncEnumerator<'T> (inner : IEnumerator<'T> ) =     

        let inner : IEnumerator<'T> = inner

        interface IAsyncEnumerator<'T> with
            member this.Current with get() = inner.Current
            member this.MoveNextAsync () = ValueTask<bool>(Task.FromResult(inner.MoveNext()))
            member this.DisposeAsync () = ValueTask(Task.FromResult(inner.Dispose))

    type TestAsyncEnumerable<'T> =       
        inherit EnumerableQuery<'T>

        new (enumerable : IEnumerable<'T>) = 
            { inherit EnumerableQuery<'T> (enumerable) }
        new (expression : Expression) = 
            { inherit EnumerableQuery<'T> (expression) }

        interface IAsyncEnumerable<'T> with
            member this.GetAsyncEnumerator cancellationToken : IAsyncEnumerator<'T> =
                 new TestAsyncEnumerator<'T>(this.AsEnumerable().GetEnumerator())
                 :> IAsyncEnumerator<'T>

        interface IQueryable<'T> with
            member this.Provider with get() = new TestAsyncQueryProvider<'T>(this) :> IQueryProvider

    and 
        TestAsyncQueryProvider<'TEntity> 
        (inner : IQueryProvider) =       

        let inner : IQueryProvider = inner

        interface IAsyncQueryProvider with

            member this.Execute (expression : Expression) =
                inner.Execute expression

            member this.Execute<'TResult> (expression : Expression) =
                inner.Execute<'TResult> expression

            member this.ExecuteAsync<'TResult> ((expression : Expression), cancellationToken) =
                inner.Execute<'TResult> expression

            member this.CreateQuery (expression : Expression) =
                new TestAsyncEnumerable<'TEntity>(expression) :> IQueryable

            member this.CreateQuery<'TElement> (expression : Expression) =
                new TestAsyncEnumerable<'TElement>(expression) :> IQueryable<'TElement>


    let getQueryableMockDbSet<'T when 'T : not struct>
        (sourceList : 'T seq) : Mock<DbSet<'T>> =

        let queryable = sourceList.AsQueryable();

        let dbSet = new Mock<DbSet<'T>>()

        dbSet.As<IAsyncEnumerable<'T>>()
            .Setup(fun m -> m.GetAsyncEnumerator())
            .Returns(TestAsyncEnumerator<'T>(queryable.GetEnumerator())) |> ignore

        dbSet.As<IQueryable<'T>>()
            .SetupGet(fun m -> m.Provider)
            .Returns(TestAsyncQueryProvider<'T>(queryable.Provider)) |> ignore

        dbSet.As<IQueryable<'T>>().Setup(fun m -> m.Expression).Returns(queryable.Expression) |> ignore
        dbSet.As<IQueryable<'T>>().Setup(fun m -> m.ElementType).Returns(queryable.ElementType) |> ignore
        dbSet.As<IQueryable<'T>>().Setup(fun m -> m.GetEnumerator ()).Returns(queryable.GetEnumerator ()) |> ignore
        dbSet

Можливо, ви захочете задати нове запитання, яке стосується F #, і відповісти одним. Він тут не належить.
ilkerkaran

3
У питанні мова не йдеться. Я також покращив прийняту відповідь, дотримуючись останнього API. Я не думаю, що це заслуговує на знижку, оскільки це корисно. Я додаю додаткову інформацію для програмістів .Net, які приземляються тут.
Райан

2
Він містить тег C #
ilkerkaran

1
У будь-якому випадку, чоловіче, я нікому не зашкоджую і не затьмарюю тему, я додаю більше корисної інформації, яка безпосередньо стосується питання. Я думаю, що ви були несправедливими, але це, напевно, ваше припущення.
Райан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.