Development Tip

Entity Framework Core를 사용하여 비동기 리포지토리를 모의하는 방법

yourdevel 2020. 12. 11. 20:23
반응형

Entity Framework Core를 사용하여 비동기 리포지토리를 모의하는 방법


비동기 저장소를 호출하는 클래스에 대한 단위 테스트를 만들려고합니다. 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.

이 작업을 수행하기 위해 저장소를 올바르게 모의하려면 어떻게해야합니까?


EF 6 : https://msdn.microsoft.com/en-us/library/dn314429.aspx 에서 동일한 작업을 수행하는 예제가있는 링크를 알려준 @Nkosi에게 감사드립니다 . 이것은 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);
}

내 Moq / NSubstitute / FakeItEasy 확장 MockQueryable을 사용해보십시오 : https://github.com/romantitov/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 버전에서도 지원됩니다.
  • 1.1.0 버전에서 지원되는 DbQuery
  • 3.0.0 버전에서 지원되는 EF Core 3.0

Much less code solution. Use the in-memory db context which should take care of bootstrapping all the sets for you. You no longer need to mock out the DbSet on your context but if you want to return data from a service for example, you can simply return the actual set data of the in-memory context.

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

  _db = new SaasDispatcherDbContext(optionsBuilder: options);

I'm maintaining two open-source projects that do the heavy lifting of setting up the mocks and actually emulates SaveChanges(Async).

For EF Core: https://github.com/huysentruitw/entity-framework-core-mock

For EF6: https://github.com/huysentruitw/entity-framework-mock

Both projects have Nuget packages with integration for Moq or NSubstitute.

참고URL : https://stackoverflow.com/questions/40476233/how-to-mock-an-async-repository-with-entity-framework-core

반응형