在基于时间的单元测试用例的最短时间内执行单元测试用例的选项
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
) 作为输入值传递,函数根据输入计算结果。
这不仅易于进行单元测试,还使您能够执行模拟(测试本质上是模拟)并计算过去的结果。
如果在 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
) 作为输入值传递,函数根据输入计算结果。
这不仅易于进行单元测试,还使您能够执行模拟(测试本质上是模拟)并计算过去的结果。