65.9K
CodeProject 正在变化。 阅读更多。
Home

使时间相关的特性可测试

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2020年10月17日

MIT

6分钟阅读

viewsIcon

5015

downloadIcon

70

描述一种使与时间相关的代码可单元测试的技术。

时间测试问题及解决方案

让我们从一个实践中经常遇到的简单场景开始。假设您有一个简单的对象——一个 `Token` ——它有一个生命周期,因此它的状态依赖于当前时间。令牌在创建后立即有效,但在 20 分钟后过期

public class Token {
  DateTime _created; 
  
  public Token() {
    _created = DateTime.UtcNow; 
  }
  
  public bool IsValid() {
    return DateTime.UtcNow < _created.AddMinutes(20);
  }
}

代码很简单,但实际中的令牌可能要复杂得多。现在的问题是——如何对其进行单元测试?直接解决方案

  var token = new Token();
  Assert.IsTrue(token.IsValid());
  Thread.Sleep(21 * 60 * 1000); // wait 21 minutes
  Assert.IsFalse(token.IsValid());  

这显然是荒谬的。没有人会接受一个需要等待 20 分钟的“测试”。我们可以将常量 20(分钟)移到一个设置对象中(我们应该这样做),然后在测试时尝试设置一个更小的值,但这并没有完全解决问题。在实际应用程序中,有许多设置,许多与时间相关的常量,这样做可能会变得困难,而且还会引入显著改变测试环境的风险(我们测试的和我们在生产中运行的)。

在手动测试中,对于整个应用程序,QA 工程师有时会使用一些变通方法。假设这个令牌是登录时生成的访问令牌。测试人员登录,检查她是否可以访问应用程序,然后快速更改计算机上的当前时间(将其向前移动 30 分钟);然后再次尝试访问应用程序。令牌应该会“过期”,并且应用程序不应该可用。这是一个与机器上的时间服务进行的赛跑,该服务会定期使用网络服务设置正确的时间。无论如何,这很棘手,而且对单元测试不起作用。

那么如何让它工作呢?想法是创建一个 `DateTime.UtcNow` 的包装器,允许“偏移”应用程序的当前时间。我已经在看到一些人皱眉了——什么?您建议修改应用程序以使您的测试运行?嗯,不完全是。我们不改变应用程序逻辑。我们只是让这个逻辑可测试。我认为这种方法没什么问题。

因此,让我们引入一个新的 `AppTime static` 类,它将作为访问当前时间的新 API

public static class AppTime {
  private static TimeSpan _offset = TimeSpan.Zero;

  public static DateTime UtcNow {
    get {
      var now = DateTime.UtcNow;
      return (_offset == TimeSpan.Zero) ? now : now.Add(_offset);
    }
  }

  public static void SetOffset(TimeSpan offset) {
    _offset = offset;
  }

  public static void ClearOffset() {
    _offset = TimeSpan.Zero;
  }
}

`AppTime` 提供对当前 UTC 时间的访问,但您可以对返回的值进行永久性偏移。这实际上是一台时间机器,允许您随意“偏移”应用程序时间。

`Token` 类代码现在变为

public class Token {
  DateTime _created; 
  
  public Token() {
    _created = AppTime.UtcNow; 
  }
  
  public bool IsValid() {
    return AppTime.UtcNow < _created.AddMinutes(20);
  }
}

并且测试变得很简单

  var token = new Token();
  Assert.IsTrue(token.IsValid());
  // shift app time forward
  AppTime.SetOffset(TimeSpan.FromMinutes(21));
  Assert.IsFalse(token.IsValid());  
  AppTime.ClearOffset(); 

我想在此指出一个重要且微妙的点。如果第二个断言失败,因为 `Token.IsValid()` 方法存在 bug,会发生什么?`Assert` 将抛出异常,最后一个 `ClearOffset()` 将不会执行。这似乎不是什么大问题,但如果您运行一系列测试,那么在此失败测试之后的其他时间相关的测试也会失败——因为它们开始时的时间已经偏移。您会看到一整堆测试失败,而不知道最初的原因是这个单一的有缺陷的方法。

为避免这种情况,务必将 AppTime.ClearOffset() 放入测试类的测试清理方法中;此方法在每次测试后执行,并将保证测试之间的隔离!

总体策略如下——在您的应用程序和库中的所有地方都使用 `AppTime.UtcNow`,而不是 `DateTime.UtcNow`。这将为您的整个应用程序提供一台时间机器。 `AppTime` 类,包含注释的完整列表,已包含在本文的下载 zip 文件中。

在手动测试中使用 Apptime

乍一看,这种技术的好处似乎仅限于基于代码的自动化测试。手动测试,通过 UI 点击——测试人员无法访问服务器上的 `AppTime` 类,那么她该如何使用它呢?实际上有一个方法。

让我们回到登录令牌的例子。QA 人员尝试在 Web ASP.NET Core 应用程序中测试登录和登录过期功能。她登录,然后尝试检查访问令牌是否在 20 分钟后过期。您,开发者,可以提供一个只有在测试环境中启用的秘密 URL,后面有一个控制器方法,允许以某种形式(URL 参数)设置时间偏移量(另有一个清除偏移量的选项)。因此,QA 工程师登录,打开另一个浏览器标签页,访问时间偏移 URL,将时间向前移动 30 分钟。然后回到应用程序标签页尝试点击某些内容——她应该被拒绝,登录令牌已过期。测试通过!

奖励 - Apptime 类中的时间测量方法

`AppTime` 类提供了一些方便的方法,可以简化测量代码跨度的精确执行时间。这是一个典型的代码片段,用于执行和测量时间序列

  // measuring operation time
  var sw = new Stopwatch();
  sw.Start();
  
  DoStuff();  
  
  sw.Stop();
  var opTime = sw.Elapsed; 

`AppTime` 类提供实用方法,可以稍微简化这一点

  // measuring operation time
  var start = AppTime.GetTimestamp();
  DoStuff();  
  var opTime = AppTime.GetDuration(start); 

原来 `Stopwatch` 类有几个 `static` 方法,您可以使用这些方法仅测量时间间隔,精确到亚毫秒,而无需创建 `Stopwatch` 实例。

当然,改进不大——原始版本需要 4 行,现在只需要 2 行。但我觉得第二种版本吸引人之处在于,它在方法上下文中似乎对精神干扰更小。当我们阅读代码时,我们希望专注于主要功能,方法的作用;时间测量方面很重要,但它本质上是一个副功能,是监控代码。副功能代码的干扰越少越好。在原始代码中,“`new`”运算符会立即引起注意,您必须花费零点几秒的精神努力来意识到它是用于时间测量的,而不是主流功能。使用 `AppTime` 方法,似乎立即就清楚了它的作用,因此它很快就被忽略了。当然,这取决于个人感知,甚至品味。无论如何,如果您喜欢就用它。

Using the Code

随附的代码是一个非常简单的控制台应用程序,其中包含 `AppTime` 类的完整列表。

关注点

我们在所有示例中都使用了 `UtcNow`(UTC 时间)。 `DateTime` 类还提供了 `Now` 属性(本地时间),`AppTime` 类也有(偏移后的本地时间)。但是,我强烈建议您永远不要使用这些属性。我们的大部分代码都在服务器上运行,代码不应该依赖于服务器的位置/时间。云中的服务器始终是 UTC 时间,无论物理位置如何。使用 UTC 时间,这样即使您在开发笔记本电脑上运行/测试,您的代码也会与生产服务器上一样工作。总而言之:避免使用本地时间,除非您绝对必须使用它;这些情况主要应限于在 UI 中显示日期时间值。

历史

  • 2020年10月17日:初始版本
© . All rights reserved.