在基于时间的单元测试用例的最短时间内执行单元测试用例的选项

Options to execute unit Test cases in minimum Time for Time Based Unit Test cases

如果在 15 分钟内进行了 3 次失败尝试,我需要锁定用户。该帐户将在一段时间后自动解锁。现在我将参数 - 最大尝试次数、锁定 window 持续时间和锁定期限作为参数传递给实现该功能的 class。即使参数值为 2s 或 3s,也会导致单元测试套件执行完成约 30 秒。

在这些场景中是否有使用特定的方法或策略来减少测试执行时间?

时间是输入

If you don't consider time an input value, think about it until you do -- it is an important concept -- John Carmack

安排您的设计,以便您可以检查决定做什么的复杂逻辑,而不是实际执行它。

将实际执行的部分设计为“如此简单,显然没有缺陷”。偶尔检查一下代码,或者作为系统测试的一部分——这些测试仍然可能会“缓慢”,但它们不会妨碍(因为你已经找到了一种更有效的方法来减轻“决策与反馈之间的差距")。

有几个选项:

  • 使用 Test Double 并注入测试可以控制的 IClock
  • 使用小于秒的时间分辨率。也许以毫秒而不是秒为单位定义 window 和隔离期。
  • 将逻辑写成一个或多个pure functions

纯函数是 intrinsically testable,所以让我对此进行扩展。

纯函数

为了确保我们使用的是纯函数,我将在 Haskell 中编写测试和被测系统。

我假设存在一些函数来检查单次登录尝试是否成功。这是如何工作的是一个单独的问题。我将对这样一个函数的输出进行建模:

data LoginResult = Success | Failure deriving (Eq, Show)

换句话说,登录尝试要么成功要么失败。

您现在需要一个函数来确定给定登录尝试和先前状态的新状态。

评价登录

起初我以为这会更复杂,但 TDD 的好处是一旦你编写了第一个测试,你就会意识到简化的潜力。很快,我意识到所需要的只是一个像这样的函数:

evaluateLogin :: (UTCTime, LoginResult) -> [UTCTime] -> [UTCTime]

这个函数需要一个当前的 LoginResult 和创建的时间(作为一个元组:(UTCTime, LoginResult)),以及以前失败的日志,和 returns 一个新的失败日志。

经过几次迭代,我写了这个 inlined HUnit parametrised test:

"evaluate login" ~: do
  (res, state, expected) <-
    [
      ((at 2022 5 16 17 29, Success), [],
        [])
      ,
      ((at 2022 5 16 17 29, Failure), [],
        [at 2022 5 16 17 29])
      ,
      ((at 2022 5 16 18 6, Failure), [at 2022 5 16 17 29],
        [at 2022 5 16 18 6, at 2022 5 16 17 29])
      ,
      ((at 2022 5 16 18 10, Success), [at 2022 5 16 17 29],
        [])
    ]
  let actual = evaluateLogin res state
  return $ expected ~=? actual

我发现有用的逻辑是,只要有测试失败,evaluateLogin 函数就会将失败时间添加到失败日志中。另一方面,如果登录成功,它会清除失败日志:

evaluateLogin :: (UTCTime, LoginResult) -> [UTCTime] -> [UTCTime]
evaluateLogin (   _, Success)          _ = []
evaluateLogin (when, Failure) failureLog = when : failureLog

但是,这并没有告诉您有关用户隔离状态的任何信息。另一个函数可以解决这个问题。

隔离状态

以下参数化测试是多次迭代的结果:

"is locked out" ~: do
  (wndw, p, whn, l, expected) <-
    [
      (ndt 0 15, ndt 1 0, at 2022 5 16 19 59, [], False)
      ,
      (ndt 0 15, ndt 1 0, at 2022 5 16 19 59, [
        at 2022 5 16 19 54,
        at 2022 5 16 19 49,
        at 2022 5 16 19 45
      ],
        True)
      ,
      (ndt 0 15, ndt 1 0, at 2022 5 16 19 59, [
        at 2022 5 16 19 54,
        at 2022 5 16 19 49,
        at 2022 5 16 18 59
      ],
        False)
      ,
      (ndt 0 15, ndt 1 0, at 2022 5 16 19 59, [
        at 2022 5 16 19 54,
        at 2022 5 16 19 52,
        at 2022 5 16 19 49,
        at 2022 5 16 19 45
      ],
        True)
      ,
      (ndt 0 15, ndt 1 0, at 2022 5 16 20 58, [
        at 2022 5 16 19 54,
        at 2022 5 16 19 49,
        at 2022 5 16 19 45
      ],
        False)
    ]
  let actual = isLockedOut wndw p whn l
  return $ expected ~=? actual

这些测试推动了以下实施:

isLockedOut :: NominalDiffTime -> NominalDiffTime -> UTCTime -> [UTCTime] -> Bool
isLockedOut window quarantine when failureLog =
  case failureLog of
    [] -> False
    xs ->
      let latestFailure = maximum xs
          windowMinimum = addUTCTime (-window) latestFailure
          lockOut = 3 <= length (filter (windowMinimum <=) xs)
          quarantineEndsAt = addUTCTime quarantine latestFailure
          isStillQuarantined = when < quarantineEndsAt
      in
        lockOut && isStillQuarantined

由于它是一个纯函数,它可以完全根据输入确定性地计算隔离状态。

决定论

None以上函数依赖于系统时钟。相反,您将当前时间 (when) 作为输入值传递,函数根据输入计算结果。

这不仅易于进行单元测试,还使您能够执行模拟(测试本质上是模拟)并计算过去的结果。