Menu / szukaj

EntityFramework – testowanie zapytań asynchonicznych

Jakiś czas temu opisałem w jaki sposób można zamockować typy DbContext przy pomocy MoqMockowanie typów DbContext oraz DbSet z wykorzystaniem Moq. Temat ten nie został wtedy całkowicie wyczerpany. Pozostał jeden element do opisania – zapytania asynchoroniczne. Do tego elementu chciałbym dziś wrócić.

Punktem wyjście będzie poprzedni wpis – czyli mamy fragment kodu, który pozwala na zamockowanie DbSet<T>. Teraz tylko dodamy możliwość obsługi wywołań asynchronicznych. Aby to zrobić należy zaimplementować interfejs IDbAsyncQueryProvider:

public class InMemoryAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
{
  private readonly IQueryProvider innerQueryProvider;
  
  internal InMemoryAsyncQueryProvider(IQueryProvider innerQueryProvider)
  {
    this.innerQueryProvider = innerQueryProvider;
  }

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

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

  public object Execute(Expression expression)
  {
    return this.innerQueryProvider.Execute(expression);
  }
  
  public TResult Execute<TResult>(Expression expression)
  {
    return this.innerQueryProvider.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));
  }
}

Implementacja ta wykorzystuje dwie dodatkowe pomocnicze klasy:

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

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

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

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

  IQueryProvider IQueryable.Provider => new InMemoryAsyncQueryProvider<T>(this);
}

public class InMemoryDbAsyncEnumerator<T> : IDbAsyncEnumerator<T>
{
  private readonly IEnumerator<T> innerEnumerator;

  public InMemoryDbAsyncEnumerator(IEnumerator<T> enumerator)
  {
    this.innerEnumerator = enumerator;
  }

  public void Dispose()
  {
    this.innerEnumerator.Dispose();
  }

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

  public T Current => this.innerEnumerator.Current;

  object IDbAsyncEnumerator.Current => this.Current;
}

W tym momencie można spróbować wykorzystać napisany kod w testach:

public async Task GetLockedUsers_Invoke_LockedUsers_v1()
{
  // Arrange
  var fixture = new Fixture();
  var lockedUser = fixture.Build<User>().With(u => u.AccountLocked, true).Create();
  var users = new List<User>
  {
    lockedUser,
    fixture.Build<User>().With(u => u.AccountLocked, false).Create(),
    fixture.Build<User>().With(u => u.AccountLocked, false).Create()
  }.AsQueryable();
  
  var usersMock = new Mock<DbSet<User>>();
  usersMock.As<IDbAsyncEnumerable<User>>()
    .Setup(m => m.GetAsyncEnumerator())
    .Returns(new InMemoryDbAsyncEnumerator<User>(users.GetEnumerator()));
  usersMock.As<IQueryable<User>>()
    .Setup(m => m.Provider)
    .Returns(new InMemoryAsyncQueryProvider<User>(users.Provider));
  usersMock.As<IQueryable<User>>()
    .Setup(m => m.Expression).Returns(users.Expression); 
  usersMock.As<IQueryable<User>>()
    .Setup(m => m.ElementType).Returns(users.ElementType);
  usersMock.As<IQueryable<User>>()
    .Setup(m => m.GetEnumerator()).Returns(users.GetEnumerator());

  var userContextMock = new Mock<UsersContext>();
  userContextMock.Setup(x => x.Users).Returns(usersMock.Object);
  var usersService = new UsersService(userContextMock.Object);

  // Act
  var lockedUsers = await usersService.GetLockedUsersAsync();

  // Assert
  Assert.Equal(new List<User> { lockedUser }, lockedUsers);
}

Oczywiście można rozszerzyć napisaną wcześniej metodę:

private static Mock<DbSet<T>> CreateDbSetMock<T>(IEnumerable<T> elements) where T : class
{
  var elementsAsQueryable = elements.AsQueryable();
  var dbSetMock = new Mock<DbSet<T>>();
  dbSetMock.As<IDbAsyncEnumerable<T>>()
    .Setup(m => m.GetAsyncEnumerator())
    .Returns(new InMemoryDbAsyncEnumerator<T>(elementsAsQueryable.GetEnumerator()));
  dbSetMock.As<IQueryable<User>>()
    .Setup(m => m.Provider)
    .Returns(new InMemoryAsyncQueryProvider<T>(elementsAsQueryable.Provider));
  dbSetMock.As<IQueryable<T>>()
    .Setup(m => m.Expression).Returns(elementsAsQueryable.Expression);
  dbSetMock.As<IQueryable<T>>()
    .Setup(m => m.ElementType).Returns(elementsAsQueryable.ElementType);
  dbSetMock.As<IQueryable<T>>()
    .Setup(m => m.GetEnumerator()).Returns(elementsAsQueryable.GetEnumerator());
  return dbSetMock;
}

Dzięki temu można skrócić test do:

[Fact]
public async Task GetLockedUsersAsync_Invoke_LockedUsers_v2()
{
  // Arrange
  var fixture = new Fixture();
  var lockedUser = fixture.Build<User>().With(u => u.AccountLocked, true).Create(); 
  IList<User> users = new List<User>
  {
    lockedUser,
    fixture.Build<User>().With(u => u.AccountLocked, false).Create(),
    fixture.Build<User>().With(u => u.AccountLocked, false).Create()
  };

  var usersMock = CreateDbSetMock(users);
  var userContextMock = new Mock<UsersContext>();
  userContextMock.Setup(x => x.Users).Returns(usersMock.Object); 
  var usersService = new UsersService(userContextMock.Object); 

  // Act
  var lockedUsers = await usersService.GetLockedUsersAsync();

  // Assert
  Assert.Equal(new List<User> {lockedUser}, lockedUsers);
}

Dodaj komentarz

imię*

e-mail* (nie publikowany)

strona www