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

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

    }

}

最后我希望有这种用法,但我不知道如何更改缓存类或任何其他帮助之王都会非常有帮助。

但它行不通,我不知道如何正确编写单元测试。


我在使用此类存储库的地方有很多此类缓存。在这种情况下,是否有任何标准或最佳实践如何对静态异步缓存进行单元测试以及如何模拟存储库?


白板的微信
浏览 81回答 1
1回答

慕容森

通过使缓存静态化,您关闭了一些门。快速而肮脏的解决方案:因为你不能构造函数注入你的存储库,下一个最好的办法就是将它传递给你的静态方法。&nbsp;public static async Task<List<Contact>> GetCodeValuesAsync(IContactRepository repo, int inst, CancellationToken ct)如果您这样做,将存储库的生命周期管理上移一个级别可能是一个更好的主意。换句话说,将using语句移动到调用者:using(var repo = new ContactRepository()){&nbsp; &nbsp; await ContactsCache.GetContactsAsync(repo , It.IsAny<int>(), CancellationToken.None);}然后在你的测试中你可以这样做:var mock = new Mock<IContactsRepository>()&nbsp; &nbsp; &nbsp; &nbsp; .Setup(x => x.LoadAsync(It.IsAny<int>(), CancellationToken.None))&nbsp; &nbsp; &nbsp; &nbsp; .ReturnsAsync(new List<Contact>(expected));var actual = await ContactsCache.GetContactsAsync(mock , It.IsAny<int>(), CancellationToken.None);优选解决方案:我假设您的存储库负责会话管理(因此使用 IDisposable 接口)。如果有一种方法可以将存储库接口与某些实现可能需要释放的任何资源分开,您可以转向构造函数注入方法。您的代码将类似于以下内容:public class ContactsCache : IContactsCache{&nbsp; &nbsp; private readonly IContactRepository contactRepo;&nbsp; &nbsp; public ContactsCache(IContactRepository contactRepo)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; this.contactRepo = contactRepo;&nbsp; &nbsp; }&nbsp; &nbsp; // ...&nbsp; &nbsp; return await this.contactRepo.LoadAsync(inst, ct).ConfigureAwait(false);&nbsp; &nbsp; // ...}您的单元测试将如下所示:[TestMethod]public async void GetContactAsync_WhenCalled_ReturnCodeValuesCache(){&nbsp; &nbsp; var expected = new List<Contact>&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; new Contact() {Instance = 1, Name = "Test" }&nbsp; &nbsp; };&nbsp; &nbsp; var mock = new Mock<IContactsRepository>()&nbsp; &nbsp; &nbsp; &nbsp; .Setup(x => x.LoadAsync(It.IsAny<int>(), CancellationToken.None))&nbsp; &nbsp; &nbsp; &nbsp; .ReturnsAsync(new List<Contact>(expected));&nbsp; &nbsp; var cache = new ContactsCache(mock);&nbsp; &nbsp; var actual = await cache .GetContactsAsync(It.IsAny<int>(), CancellationToken.None);&nbsp; &nbsp; CollectionAssert.AreEqual(actual, expected);}您还可以考虑反转缓存和存储库之间的依赖关系。换句话说,您的存储库实现可以有一个缓存。这允许您更动态地选择缓存策略。例如,您可能有以下任一情况:var repo = new ContactRepository(new MemoryCache<Contact>())或者var repo = new ContactsRepository(new NullCache<Contact>())<-- 如果您在某些情况下不需要缓存。这种方法意味着存储库的使用者不需要知道或关心数据的来源。这使您可以在一开始就不需要存储库的情况下测试缓存机制。当然,如果您想测试存储库,则需要为其提供缓存策略。遵循这种方法还可以让您获得一个相当快速的解决方案,因为您可以用这样的类包装现有的静态缓存:public class MemoryCache : ICachingStrategy<Contact>{&nbsp; &nbsp; public async Task<List<Contact>> GetCodeValuesAsync(int inst, CancellationToken ct) // This comes from the interface&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; return await ContactsCache.GetContactsAsync(inst, ct); // Just forward the call to the existing static cache&nbsp; &nbsp; }}您的存储库需要做一些工作才能使其在访问数据库/文件系统/远程资源之前考虑缓存。旁注——如果你new设置了“依赖项”,你就不再进行依赖注入了。
打开App,查看更多内容
随时随地看视频慕课网APP