Jakiś czas temu opisałem w jaki sposób można zamockować typy DbContext przy pomocy Moq – Mockowanie 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ścia 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); }
Zostaw komentarz