静态异步类的模拟单元测试和依赖项注入

时间:2019-04-01 15:59:37

标签: c# .net unit-testing testing

我有一个用于联系人列表的静态异步缓存。在缓存内部,我正在调用我的存储库以从后端获取数据。我想模拟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 个答案:

答案 0 :(得分: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建立“依赖项”,则不再进行依赖项注入。