首页 > 解决方案 > 调用EF Core扩展方法的c#单元测试方法

问题描述

我一直在尝试对这个简单的方法进行单元测试:

public void DeleteAllSettingsLinkedToSoftware(Guid softwareId)
{
    _dbContext.Settings.Where(s => s.SoftwareId == softwareId).ForEachAsync(s => s.IsDeleted = true);
    _dbContext.SaveChanges();
}

ForEachAsync()但是,从调用该方法的那一刻起,我很难对该方法进行单元测试。

到目前为止,我已经使用 Moq 来设置 dbContext 以在Where()执行时返回正确的设置。我的尝试:

Setup(m => m.ForEachAsync(It.IsAny<Action<Setting>>(), CancellationToken.None));

我的问题是:我将如何对ForEachAsync()方法的调用进行单元测试?

我在网上读到有人说不可能对某些静态方法进行单元测试,如果在我的情况下这是真的,我对尽可能多地测试这种方法的替代方案感到好奇。

编辑

我的完整测试代码:

[TestMethod]
public async Task DeleteAllSettingsLinkedToSoftware_Success()
{
    //Arrange
    var settings = new List<Setting>
    {
        new Setting
        {
            SoftwareId = SoftwareId1
        },
        new Setting
        {
            SoftwareId = SoftwareId1
        },
        new Setting
        {
            SoftwareId = SoftwareId1
        },
        new Setting
        {
            SoftwareId = SoftwareId2
        }
    }.AsQueryable();

    var queryableMockDbSet = GetQueryableMockDbSet(settings.ToList());
    queryableMockDbSet.As<IQueryable<Setting>>()
        .Setup(m => m.Provider)
        .Returns(new TestDbAsyncQueryProvider<Setting>(settings.Provider));

    DbContext.Setup(m => m.Settings).Returns(queryableMockDbSet.Object);

    _settingData = new SettingData(DbContext.Object, SettingDataLoggerMock.Object);

    //Act
    var result = await _settingData.DeleteAllSettingsLinkedToSoftwareAsync(SoftwareId1);

    //Assert
    DbContext.Verify(m => m.Settings);
    DbContext.Verify(m => m.SaveChanges());
    Assert.AreEqual(4, DbContext.Object.Settings.Count());
    Assert.AreEqual(SoftwareId2, DbContext.Object.Settings.First().SoftwareId);

}

我知道我的 Assert 仍然需要更多检查。

GetQueryableMockDbSet 方法:

public static Mock<DbSet<T>> GetQueryableMockDbSet<T>(List<T> sourceList) where T : class
{
    var queryable = sourceList.AsQueryable();

    var dbSet = new Mock<DbSet<T>>();
    dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
    dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
    dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
    dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
    dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>(s => sourceList.Add(s));
    dbSet.Setup(d => d.AddRange(It.IsAny<IEnumerable<T>>())).Callback<IEnumerable<T>>(sourceList.AddRange);
    dbSet.Setup(d => d.Remove(It.IsAny<T>())).Callback<T>(s => sourceList.Remove(s));
    dbSet.Setup(d => d.RemoveRange(It.IsAny<IEnumerable<T>>())).Callback<IEnumerable<T>>(s =>
    {
        foreach (var t in s.ToList())
        {
            sourceList.Remove(t);
        }
    });

    return dbSet;
}

标签: c#unit-testingasynchronousmockingmoq

解决方案


你根本不必嘲笑ForEachAsyncForEachAsync返回Task并正在异步执行这是您问题的根源。

使用asyncawaitkeywards 来解决您的问题:

public async void DeleteAllSettingsLinkedToSoftware(Guid softwareId)
{
    await _dbContext.Settings.Where(s => s.SoftwareId == softwareId)
                             .ForEachAsync(s => s.IsDeleted = true);
    _dbContext.SaveChanges();  
}

编辑:

发生新异常是因为提供Provider的不是IDbAsyncQueryProvider

Microsoft 实现了此接口的通用版本:TestDbAsyncQueryProvider<TEntity>. 以下是链接中的实现:

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

现在Setup你必须像这样使用它:

mockSet.As<IQueryable<Setting>>() 
       .Setup(m => m.Provider) 
       .Returns(new TestDbAsyncQueryProvider<Setting>(data.Provider)); 

推荐阅读