Имитирующие модульные тесты и внедрение зависимостей для статического асинхронного класса

#c# #.net #unit-testing #testing

#c# #.net #модульное тестирование #тестирование

Вопрос:

У меня есть статический асинхронный кэш для списка контактов. Внутри кэша я вызываю свой репозиторий, чтобы получить данные из серверной части. Я хочу создать макет ContactsRepository, но мне нужно передать репозиторий в качестве параметра и использовать внедрение зависимостей.

Согласно документации, это не сработает, потому что мне нужен экземпляр класса для использования внедрения зависимостей.

 public interface IContactsCache
{
    Task<List<Contact>> GetContactsAsync(int inst, CancellationToken ct);
}

public class ContactsCache : IContactsCache
{
    private static readonly object _syncRoot = new object();
    private static readonly Dictionary<int, Task<List<Contact>>> _contactsTasks = new Dictionary<int, Task<List<Contact>>>();

    public static Task<List<Contact>> GetContactsAsync(int inst)
    {
        return GetContactsAsync(inst, CancellationToken.None);
    }

    public static async Task<List<Contact>> GetCodeValuesAsync(int inst, CancellationToken ct)
    {
        Task<List<Contact>> task;

        lock (_syncRoot)
        {
            if (_contactsTasks.ContainsKey(inst) amp;amp; (_contactsTasks[inst].IsCanceled || _contactsTasks[inst].IsFaulted))
            {
                _contactsTasks.Remove(inst);
            }

            if (!_contactsTasks.ContainsKey(inst))
            {
                _contactsTasks[inst] = Task.Run(async () =>
                {
                    using (var rep = new ContactsRepository())
                    {
                        return await rep.LoadAsync(inst, ct).ConfigureAwait(false);
                    }
                });
            }

            task = _contactsTasks[inst];
        }

        var res = await task.ConfigureAwait(false);

        lock (_syncRoot)
        {
            return res != null ? res.ToList() : null;
        }
    }

    Task<List<CodeValue>> IContactsCache.GetContactsAsync(int inst, CancellationToken ct)
    {
        return GetContactsAsync(inst, ct);
    }
}
  

В конце я ожидаю такого использования, но я не могу понять, как изменить класс кэша, или любая другая помощь king of будет очень полезной.

 [TestMethod]
public async void GetContactAsync_WhenCalled_ReturnCodeValuesCache()
{
    var expected = new List<Contact>
    {
        new Contact() {Instance = 1, Name = "Test" }
    };

    var mock = new Mock<IContactsRepository>()
        .Setup(x => x.LoadAsync(It.IsAny<int>(), CancellationToken.None))
        .ReturnsAsync(new List<Contact>(expected));

    var actual = await ContactsCache.GetContactsAsync(It.IsAny<int>(), CancellationToken.None);

    CollectionAssert.AreEqual(actual, expected);
}
  

Но это не может работать, и я не знаю, как правильно написать модульный тест.

У меня много таких кэшей, в которых я использую подобные репозитории. Существуют ли какие-либо стандартные или наилучшие методы модульного тестирования статических асинхронных кэшей и как в этом случае имитировать репозиторий?

Комментарии:

1. ContactsCache Обязательно ли быть статичным?

2. Какой макет фреймворка вы используете? Похоже на Moq, но я не хочу указывать неправильный тег в вопросе…

3. Зачем вообще делать класс статичным? Просто пусть это будет обычный класс. Вся задача DependencyInjection заключается в предоставлении экземпляра класса кэша любому коду, который этого требует.

4. да, я использую Moq

5. Мне нужно, чтобы он был статичным для использования во многих местах. Он используется для кэширования, и очень полезно иметь его статичным. Я могу сделать это как singelton, но не уверен, что это может помочь.

Ответ №1:

Вы закрыли некоторые двери, сделав кэш статичным.

Быстрое и грязное решение:

Поскольку вы не можете внедрить конструктор в свой репозиторий, следующим лучшим решением было бы передать его вашему статическому методу.

  public static async Task<List<Contact>> GetCodeValuesAsync(IContactRepository repo, int inst, CancellationToken ct)
  

Если вы сделаете это, возможно, было бы лучшей идеей переместить управление жизненным циклом репозитория на один уровень выше. Другими словами, переместите using инструкцию вызывающему:

 using(var repo = new ContactRepository())
{
    await ContactsCache.GetContactsAsync(repo , It.IsAny<int>(), CancellationToken.None);
}
  

Тогда в вашем тесте вы смогли бы сделать это:

 var mock = new Mock<IContactsRepository>()
        .Setup(x => x.LoadAsync(It.IsAny<int>(), CancellationToken.None))
        .ReturnsAsync(new List<Contact>(expected));

var actual = await ContactsCache.GetContactsAsync(mock , It.IsAny<int>(), CancellationToken.None);
  

Предпочтительные решения:

Я предполагаю, что ваш репозиторий отвечает за управление сеансами (отсюда и интерфейс IDisposable). Если у вас есть способ отделить интерфейс вашего репозитория от любых ресурсов, которые могут потребоваться для освобождения в некоторых реализациях, вы можете перейти к подходу внедрения конструктора.

После этого ваш код будет выглядеть примерно следующим образом:

 public class ContactsCache : IContactsCache
{
    private readonly IContactRepository contactRepo;

    public ContactsCache(IContactRepository contactRepo)
    {
        this.contactRepo = contactRepo;
    }

    // ...
    return await this.contactRepo.LoadAsync(inst, ct).ConfigureAwait(false);
    // ...
}
  

И ваш модульный тест будет выглядеть следующим образом:

 [TestMethod]
public async void GetContactAsync_WhenCalled_ReturnCodeValuesCache()
{
    var expected = new List<Contact>
    {
        new Contact() {Instance = 1, Name = "Test" }
    };

    var mock = new Mock<IContactsRepository>()
        .Setup(x => x.LoadAsync(It.IsAny<int>(), CancellationToken.None))
        .ReturnsAsync(new List<Contact>(expected));

    var cache = new ContactsCache(mock);

    var actual = await cache .GetContactsAsync(It.IsAny<int>(), CancellationToken.None);

    CollectionAssert.AreEqual(actual, expected);
}
  

Вы также можете рассмотреть возможность изменения зависимости между кэшем и репозиторием. Другими словами, ваша реализация репозитория может иметь кэш. Это позволяет вам более динамично выбирать стратегию кэширования. Например, у вас может быть любой из следующих:

var repo = new ContactRepository(new MemoryCache<Contact>())

или

var repo = new ContactsRepository(new NullCache<Contact>()) <— если вам не нужно кэширование в некоторых контекстах.

Этот подход означает, что потребителю вашего репозитория не нужно знать или заботиться о том, откуда поступают данные. Это позволяет вам протестировать свой механизм кэширования, в первую очередь, не прибегая к репозиторию. Конечно, если вы хотите протестировать репозиторий, вам нужно будет предоставить ему стратегию кэширования.

Следование этому подходу также дает вам доступ к довольно быстрому решению, поскольку вы можете обернуть свой существующий статический кэш классом, подобным этому:

 public class MemoryCache : ICachingStrategy<Contact>
{
    public async Task<List<Contact>> GetCodeValuesAsync(int inst, CancellationToken ct) // This comes from the interface
    {
        return await ContactsCache.GetContactsAsync(inst, ct); // Just forward the call to the existing static cache
    }
}
  

Вашему репозиторию потребуется некоторая работа, чтобы заставить его учитывать кеш, прежде чем обращаться к базе данных / файловой системе / удаленному ресурсу.

Примечание сбоку — если вы new добавите «зависимости», вы больше не будете выполнять внедрение зависимостей.