首页 > 解决方案 > 单元测试中的配置值永远不会被设置

问题描述

我的 C# 应用程序有一个HealthHandler类,它在其构造函数中将配置作为参数。

public class HealthHandler : IHeartbeatCheckHealthHandler
{
    private readonly IApplicationTime _applicationTime;
    private readonly IConfig _config;
 
    private DateTime _time = DateTime.UtcNow;
    private readonly TimeSpan _timespan;
 
    public bool IsHealthy => (_time.Add(_timespan) > _applicationTime.UtcNow);
 
    public HealthHandler([NotNull] IConfig healthcheckOptions)
    {
        _applicationTime = new StandardManualTimer(new DateTime?(DateTime.UtcNow));
        _config = healthcheckOptions ?? throw new ArgumentNullException(nameof(healthcheckOptions));
        _timespan = TimeSpan.FromSeconds(_config.HeartbeatTimespan);
    }
 
    public void OnMessage(int value)
    {
        if (value == 10)
        {
            _time = _applicationTime.UtcNow;
        }
    }
}

我正在尝试对此进行单元测试。这是我到目前为止所得到的:

public class HealthcheckTest
{
    private IConfig _config;
 
    public HealthcheckTest()
    {
        _config = new FeedConfigOptions();
        _config.HeartbeatTimespan = 5;
    }
 
    [Fact]
    public void TestKafkaHealthStateUpdates()
    {
            
        HealthHandler healthHanler = new HealthHandler(_config);
        healthHanler.OnMessage(2);
        healthHanler.IsHealthy.Should().Be(true);
        Thread.Sleep(6000);
        healthHanler.IsHealthy.Should().Be(false);
        healthHanler.OnMessage(10);
        healthHanler.IsHealthy.Should().Be(true);
    }
 
}

每个healthHanler.IsHealthy.Should().Be();调用都返回 true。似乎我的测试从未将 HeartbeatTimespan 设置为 5,它始终默认为 0。

我的 StandardManualTimer 看起来像这样:

public class StandardManualTimer : ManualTimer<DateTime>, IApplicationTime
{
    public StandardManualTimer(DateTime? initial = null, TimerGranularity? granularity = null)
      : base(initial ?? DateTime.UtcNow, granularity)
    {
    }
 
    public void AdvanceBy(TimeSpan timeSpan) => this.AdvanceTo(this.Now.Add(timeSpan));
 
    public DateTime UtcNow => this.Now;
  }

下面是手动定时器:

public class ManualTimer<TPriority> : IManualTimer<TPriority>, ITimer<TPriority>, IDisposable
    where TPriority : IComparable<TPriority>
{
    private volatile bool _isDisposed;
 
    [NotNull]
    public TPriority Next { get; private set; }
 
    public TimerGranularity Granularity { get; }
 
    [NotNull]
    public TPriority Previous { get; private set; }
 
    public bool IsCancelled { get; private set; }
 
    public ManualTimer(TPriority initial, TimerGranularity? granularity = null)
    {
      this.Next = (object) initial != null ? initial : throw new ArgumentNullException(nameof (initial), "The initial time cannot be null");
      this.Granularity = granularity.GetValueOrDefault();
      this.Previous = initial;
      this.Now = initial;
      this.IsCancelled = true;
    }
 
    public void Dispose() => this._isDisposed = true;
 
    public void Cancel() => this.IsCancelled = true;
 
    public TimerChangeResponse RequestNext(TPriority next)
    {
      if (this._isDisposed)
        return TimerChangeResponse.Disposed;
      if (next.CompareTo(this.Now) <= 0)
        return TimerChangeResponse.FailedRetry;
      this.IsCancelled = false;
      this.Next = next;
      return TimerChangeResponse.Scheduled;
    }
 
    public void AdvanceTo(TPriority newNow)
    {
      if ((object) newNow == null)
        throw new ArgumentNullException(nameof (newNow), "Now cannot be null");
      if (this.Granularity == TimerGranularity.Fine)
      {
        while (!this.IsCancelled && this.Next.CompareTo(newNow) < 0 && this.Next.CompareTo(this.Previous) > 0)
          SetNow(this.Next);
      }
      if (this.Now.CompareTo(newNow) >= 0)
        return;
      SetNow(newNow);
 
      void SetNow(TPriority value)
      {
        this.Now = value;
        if (this.IsCancelled || this.Now.CompareTo(this.Next) < 0 || this.Next.CompareTo(this.Previous) <= 0)
          return;
        this.Previous = value;
        EventHandler nextFired = this.NextFired;
        if (nextFired == null)
          return;
        nextFired((object) this, EventArgs.Empty);
      }
    }
 
    public event EventHandler NextFired;
 
    [NotNull]
    public TPriority Now { get; private set; }
}

如何在我的测试中解决这个问题?

标签: c#.netunit-testing

解决方案


_applicationTime用当前时间初始化:

_applicationTime = new StandardManualTimer(new DateTime?(DateTime.UtcNow));

_applicationTime.UtcNow返回初始化值,Thread.Sleep则不影响_applicationTime.UtcNow. 你需要_applicationTime.AdvanceTo像这样使用:

注入IApplicationTime

public class HealthHandler : IHeartbeatCheckHealthHandler
{
    private readonly IApplicationTime _applicationTime;
    private readonly IConfig _config;

    private DateTime _time = DateTime.UtcNow;
    private readonly TimeSpan _timespan;

    public bool IsHealthy => (_time.Add(_timespan) > _applicationTime.UtcNow);

    public HealthHandler([NotNull] IConfig healthcheckOptions, IApplicationTime applicationTime)
    {
        _applicationTime = applicationTime;
        _config = healthcheckOptions ?? throw new ArgumentNullException(nameof(healthcheckOptions));
        _timespan = TimeSpan.FromSeconds(_config.HeartbeatTimespan);
    }

    public void OnMessage(int value)
    {
        if (value == 10)
        {
            _time = _applicationTime.UtcNow;
        }
    }
}

并修改测试:

[Fact]
public void TestKafkaHealthStateUpdates()
{
    var app = new StandardManualTimer(DateTime.UtcNow);
    HealthHandler healthHanler = new HealthHandler(_config, app);
    healthHanler.OnMessage(2);
    healthHanler.IsHealthy.Should().Be(true);
    Thread.Sleep(6000);
    app.AdvanceTo(DateTime.UtcNow);
    healthHanler.IsHealthy.Should().Be(false);
    healthHanler.OnMessage(10);
    healthHanler.IsHealthy.Should().Be(true);
}

奖励:你也可以模拟IApplicationTime


推荐阅读