我一直试图对这个简单的方法进行单元测试:
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;
}
答案 0 :(得分:4)
您根本不必模拟ForEachAsync
。 ForEachAsync
返回Task
并正在异步执行,这是您问题的根源。
使用async
和await
键来解决您的问题:
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行实现!
根据这种情况,您有两个选择:
在第二种情况下,我会实现这个逻辑,这样我就能编写一个类似的测试:
[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();
}
}
通过这样的解决方案,每个问题都是分开的,允许: