Mocking Unit тесты и внедрение зависимостей для статического асинхронного класса - PullRequest
0 голосов
/ 01 апреля 2019

У меня статический асинхронный кеш для списка контактов. Внутри кеша я вызываю свой репозиторий для получения данных из бэкэнда. Я хочу смоделировать 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) && (_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);
    }
}

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

[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 Ответ

1 голос
/ 02 апреля 2019

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

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

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

 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>()) <- если вам не нужно кэширование в некоторых контекстах. </p>

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

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

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

Вашему хранилищу потребуется некоторая работа, чтобы заставить его учитывать кэш, прежде чем обращаться к базе данных db / file system / remote.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...