Метод модульного тестирования c #, который вызывает метод расширения EF Core - PullRequest
0 голосов
/ 01 июня 2018

Я пытался провести модульное тестирование этого простого метода:

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 голосов
/ 12 июня 2018

Отказ от ответственности: я не буду предлагать прямое решение, как того требует OP, потому что я считаю, что этот вопрос является XY проблемой .Вместо этого я сосредоточусь на своем ответе на том, почему этот код так чертовски сложно протестировать, потому что да, более 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

Дело в том, что такие методы (которые смешивают проблемы) трудно проверить на уровне устройства.Отсюда тот факт, что вам нужны макеты и несколько десятков кода «Arrange», чтобы протестировать только 2 строки реализации!

На основании этой констатации у вас есть 2 варианта:

  1. Вы удаляете свой тестпотому что метод в основном содержит логику 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();
    }
}

При таком решении каждая проблема разделяется, что позволяет:

  • Избавиться от насмешек
  • Код бизнес-логики для действительно модульного тестирования
  • Увеличение удобства обслуживания и читаемости
0 голосов
/ 02 июня 2018

Тебе совсем не надо издеваться 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)); 
...