C# 异步初始化及其单元测试

C# 异步初始化及其单元测试,c#,.net,unit-testing,asynchronous,initialization,C#,.net,Unit Testing,Asynchronous,Initialization,背景 我需要一些类来执行后台初始化,应该从构造函数开始。目前,我正在使用一个由构造函数启动的任务,然后根据初始化情况,所有操作都将等待该任务完成 请看下面的简化示例: interface IEntry {} interface IRepository { IQueryable<IEntry> Query { get; } void Add(IEntry entry); } class PrefetchedRepository : IRepository {

背景 我需要一些类来执行后台初始化,应该从构造函数开始。目前,我正在使用一个由构造函数启动的
任务
,然后根据初始化情况,所有操作都将等待该
任务
完成

请看下面的简化示例:

interface IEntry {}

interface IRepository
{
    IQueryable<IEntry> Query { get; }
    void Add(IEntry entry);
}

class PrefetchedRepository : IRepository
{
    private readonly Task _prefetchingTask;
    private readonly ICollection<IEntry> _entries = new List<IEntry>();
    private readonly IRepository _underlyingRepository;

    public PrefetchedRepository(IRepository underlyingRepository)
    {
        _underlyingRepository = underlyingRepository;
        // Background initialization starts here
        _prefetchingTask = Task.Factory.StartNew(Prefetch);
    }

    public IQueryable<IEntry> Query
    { 
        get
        {
            EnsurePrefetchCompleted();
            return _entries.AsQueryable();
        }
    }

    public void Add(IEntry entry)
    {
        EnsurePrefetchCompleted();      
        _entries.Add(entry);
        _underlyingRepository.Add(entry);
    }

    private void EnsurePrefetchCompleted()
    {
        _prefetchingTask.Wait();
    }

    private void Prefetch()
    {
        foreach (var entry in _underlyingRepository.Query)
        {
            _entries.Add(entry);
        }
    }
}
  • EnsurePrefetchCompleted
    方法公开为内部方法,并在单元测试中使用它(假设使用
    [程序集:InternalsVisibleTo(“…”)

  • 这两种解决方案的问题是,如果出现故障,持续时间很长(实际上是在第二种情况下,它受到测试超时的限制)


    有没有更简单的方法来进行这种测试?

    将预取逻辑提取到一个单独的预取器类中,并且在测试时,使用一些不使用单独线程的方法来模拟预取器

    这将允许您对预回迁存储库进行白盒测试,我看到您正在尝试使用它
    underyingrepositorymock.CallsTo(r=>r.Query).musthavebefored(Repeated.justice(1))(我永远不会做白盒测试,但那只是我自己。)


    完成白盒测试后,就可以对预回迁存储库进行黑盒测试,而不必关心它内部的工作方式。(它是否调用其他对象来完成它的工作,调用它们的次数,等等)因此,测试代码不需要猜测检查查询是否被调用的时间点,因为它根本不关心查询是否被调用。基本上,您的测试代码将针对
    接口IRepository
    进行测试,而不是针对
    类预取存储库

    不要公开处于无效状态的实例。在调用
    PrefetchedRepository
    中的任何成员时,客户端代码可能经常会注意到延迟,这仅仅是因为底层存储库速度较慢。您正试图通过隐藏客户机代码甚至都不知道的
    EnsurePrefetchCompleted
    中所有糟糕的等待逻辑来隐藏这些细节,从而变得更聪明。但这可能会让客户感到惊讶,为什么连这都要花很多时间

    更好的方法是在API的公共界面中公开
    任务
    ,并让客户端代码在对存储库实例执行任何操作之前等待它

    大概是这样的:

    class PrefetchedRepository : IRepository
    {
        private readonly Task _prefetchingTask;
        private readonly ICollection<IEntry> _entries = new List<IEntry>();
        private readonly IRepository _underlyingRepository;
    
        public PrefetchedRepository(IRepository underlyingRepository)
        {
            _underlyingRepository = underlyingRepository;
            // Background initialization starts here
            _prefetchingTask = Task.Factory.StartNew(Prefetch);
        }
    
        public Task Initialization
        { 
            get
            {
                return _prefetchingTask;
            }
        }
        ...
    }
    
    当然,删除
    EnsurePrefetchCompleted
    方法和对它的所有调用

    但我知道这会带来所谓的气味

    更好的设计是引进一家工厂为你做这件事

    public class PrefetchedRepositoryFactory
    {
        public Task<IRepository> CreateAsync()
        {
            //someOtherSlowRepo can be a parameter or instance field of this class
            var repo = new PrefetchedRepository(someOtherSlowRepo);
            await repo.Initialization;
            return repo;
        }
    }
    
    如果您这样做,您就不必特别注意测试,因为您将始终拥有完整构建的存储库


    你可以等待里面的测试方法;大多数主要的单元测试框架都支持
    异步任务
    返回方法。

    这肯定会解决存储库测试的问题。但是在这种情况下,我应该如何测试真正的异步预取器呢;请让我知道它是否清晰。很好的解决方案,但它涉及到改变我接收存储库实例的方式。我不希望改变这一点,而是限制实施范围的改变。至于慢度和其他注意事项-
    PrefetchingRepository
    只是一些后台初始化的一个例子,我需要从构造函数开始。我真正的问题不是数据获取,只是要补充一点,如果这是一些遗留代码,我只会使用反射来调用私有EnsurePrefetchCompleted()方法。关于你的第一个问题,我描述一下/@StephenCleary,谢谢!它真的很有用!
    class PrefetchedRepository : IRepository
    {
        private readonly Task _prefetchingTask;
        private readonly ICollection<IEntry> _entries = new List<IEntry>();
        private readonly IRepository _underlyingRepository;
    
        public PrefetchedRepository(IRepository underlyingRepository)
        {
            _underlyingRepository = underlyingRepository;
            // Background initialization starts here
            _prefetchingTask = Task.Factory.StartNew(Prefetch);
        }
    
        public Task Initialization
        { 
            get
            {
                return _prefetchingTask;
            }
        }
        ...
    }
    
    var repo = new PrefetchedRepository(someOtherSlowRepo);
    await repo.Initialization;
    //Then do whatever with the repo.
    
    public class PrefetchedRepositoryFactory
    {
        public Task<IRepository> CreateAsync()
        {
            //someOtherSlowRepo can be a parameter or instance field of this class
            var repo = new PrefetchedRepository(someOtherSlowRepo);
            await repo.Initialization;
            return repo;
        }
    }
    
    var repo = await prefetchedRepositoryFactory.CreateAsync();
    //Do whatever with repo.