如何在 Threading.Timer 周围制作可测试的包装器并在单元测试时替换它?

How to make a testable wrapper around Threading.Timer and replace it when unit testing?

我已经为 System.Threading.Timer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period); 过载测试苦苦挣扎了一天多。

基于 this solution 我创建了 ThreadingTimerFakeTimer,它实现了 ITimer:

public interface ITimer
    {
        bool Change(int dueTime, int period);
        bool IsDisposed { get; }

        void Dispose();
    }


public class ThreadingTimer : ITimer, IDisposable
    {
        private Timer _timer;

        public ThreadingTimer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period)
        {
            _timer = new Timer(callback, state, dueTime, period);
        }

        public bool IsDisposed { get; set; }

        public void Dispose()
        {
            IsDisposed = true;
            Dispose();
            GC.SuppressFinalize(this);
        }


        public bool Change(int dueTime, int period)
        {
            _timer.Change(dueTime, period);

            return true;
        }
    }

public class FakeTimer : ITimer
    {
        private object state;

        public FakeTimer(TimerCallback callback, object state)
        {
            this.state = state;
        }

        public bool IsDisposed { get; set; }

        public bool Change(int dueTime, int period)
        {
            return true;
        }

        public void Dispose()
        {
            IsDisposed = true;
            Dispose();
            GC.SuppressFinalize(this);
        }
    }

在我的服务中 class 我想使用我的 ThreadingTimer:

public ITimer Timer { get; set; }
private void StartUpdates()
        {
            Task.Run(() =>
            {
                Timer = new ThreadingTimer(StartUpdatingVessels, null, TimeSpan.Zero, TimeSpan.FromHours(24));

            }, Token);
        }

但是当涉及到单元测试时,我不知道也不了解如何在我的 FakeTimer[=37 中使用和利用 ITimer 的实现=].因为如果在我的测试中将调用方法 StartUpdates(),将创建 ThreadingTimer 的新实例,即使已经分配了 Timer prop 假定时器:

[Fact]
        public void SetUpdatingStarted_CalledWhenUpdatingAlreadyStarted_DisposesTimer()
        {
            _timedUpdateControl.Timer = new FakeTimer(CallbackTestMethod, null);
            bool intendedToStartUpdates = false;

            _timedUpdateControl.StartOrStopUpdates(intendedToStartUpdates); //that methid calls private method StartUpdates() and creates instance for ITimer

            //assert something
        }

我怎么在那里嘲笑它? (我在测试项目中使用 Moq 框架)。

一种解决方案是使用依赖注入:

public interface ITimer
{
    event Action Elapsed;
    void StartTimer(int dueTime, int period);
    bool IsDisposed { get; }

    void Dispose();
}

public class TimedUpdateControl
{
    private ITimer timer;

    public TimedUpdateControl(ITimer timer)
    {
        this.timer = timer;
        this.timer.Elapsed += () => { // do something }
    }

    private void StartUpdates()
    {
        Task.Run(() =>
        {
            timer.StartTimer(TimeSpan.Zero, TimeSpan.FromHours(24));
        }, Token);
    }
}

测试:

[Fact]
public void SetUpdatingStarted_CalledWhenUpdatingAlreadyStarted_DisposesTimer()
{
    var timedUpdateControl = new TimedUpdateControl(new FakeTimer());
    bool intendedToStartUpdates = false;

    timedUpdateControl.StartOrStopUpdates(intendedToStartUpdates); 

    //assert something
}