c#单元测试方法,调用EF Core扩展方法

时间:2018-06-01 14:00:55

标签: c# unit-testing asynchronous mocking moq

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

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

2 个答案:

答案 0 :(得分:4)

您根本不必模拟ForEachAsyncForEachAsync返回Task并正在异步执行,这是您问题的根源。

使用asyncawait键来解决您的问题:

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

答案 1 :(得分:1)

免责声明:根据OP的要求,我不会提供直接的解决方案,因为我认为这个问题是XY problem。相反,我将集中我的答案为什么这段代码很难测试,因为是的,超过30行“安排”测试2行代码意味着出了问题。

简短回答

至少在单位级别,不需要测试此方法。

答案很长

当前实施的问题是一个令人担忧的问题。

第一行:_dbContext.Settings.Where(s => s.SoftwareId == softwareId).ForEachAsync(s => s.IsDeleted = true);包含业务逻辑(s.softwareId == softwareId,s.IsDeleted = true),还包含EF逻辑(_dbContext,ForEachAsync)。

第二行:_dbContext.SaveChanges();仅包含EF逻辑

重点是:这种方法(混合问题)难以在单位级别进行测试。因此,你需要模拟和几十个“安排”代码来测试只有2行实现!

根据这种情况,您有两个选择:

  1. 您删除了测试,因为该方法主要包含无法测试的EF逻辑(有关详细信息,请参阅this great series of articles
  2. 您提取业务逻辑并编写真实(简单)单元测试
  3. 在第二种情况下,我会实现这个逻辑,这样我就能编写一个类似的测试:

    [Test]
    public void ItShouldMarkCorrespondingSettingsAsDeleted()
    {
        var setting1 = new Setting(guid1);
        var setting2 = new Setting(guid2);
        var settings = new Settings(new[] { setting1, setting2 });
    
        settings.DeleteAllSettingsLinkedToSoftware(guid1);
    
        Assert.That(setting1.IsDeleted, Is.True);
        Assert.That(setting1.IsDeleted, Is.False);
    }
    

    易于书写,易于阅读。

    现在实施怎么样?

    public interface ISettings
    {
        void DeleteAllSettingsLinkedToSoftware(Guid softwareId);
    }
    
    public sealed class Settings : ISettings
    {
        private readonly IEnumerable<Setting> _settings;
        public Settings(IEnumerable<Setting> settings) => _settings = settings;
        public override void DeleteAllSettingsLinkedToSoftware(Guid softwareGuid)
        {
            foreach(var setting in _settings.Where(s => s.SoftwareId == softwareId))
            {
                setting.IsDeleted = true;
            }
        }
    }
    
    public sealed class EFSettings : ISettings
    {
        private readonly ISettings _source;
        private readonly DBContext _dbContext;
        public EFSettings(DBContext dbContext)
        {
            _dbContext = dbContext;
            _source = new Settings(_dbContext.Settings);
        }
        public override void DeleteAllSettingsLinkedToSoftware(Guid softwareGuid)
        {
            _source.DeleteAllSettingsLinkedToSoftware(softwareGuid);
            _dbContext.SaveChanges();
        }
    }
    

    通过这样的解决方案,每个问题都是分开的,允许:

    • 摆脱嘲笑
    • 真正单元测试业务逻辑代码
    • 获得可维护性和可读性