#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
добавите «зависимости», вы больше не будете выполнять внедрение зависимостей.