【问题标题】:Is there an easy way to stub out time.Now() globally during test?有没有一种简单的方法可以在测试期间全局删除 time.Now() ?
【发布时间】:2022-01-19 00:42:18
【问题描述】:

我们的部分代码是时间敏感的,我们需要能够保留一些东西,然后在 30-60 秒等内释放它,我们可以这样做 time.Sleep(60 * time.Second)

我刚刚实现了时间接口,并且在测试期间使用了时间接口的存根实现,类似于this golang-nuts discussion

但是,time.Now() 在多个站点中被调用,这意味着我们需要传递一个变量来跟踪我们实际睡了多少时间。

我想知道是否有另一种方法可以在全球范围内存根 time.Now()。是否可以进行系统调用来更改系统时钟?

也许我们可以编写自己的时间包,它基本上包含时间包但允许我们更改它?

我们当前的实现运行良好,我是一个围棋初学者,我很想知道是否有人有其他想法?

【问题讨论】:

    标签: unit-testing go


    【解决方案1】:

    如果您不介意额外的依赖,有一个易于使用的clock library for mocking time in Go,它封装了标准库,因此可以在测试中轻松地模拟它。

    【讨论】:

      【解决方案2】:

      你也可以使用 go playground 的 faketime 方法。 它将保留一个内部“时钟”值来代替time.Now(),并从任何对time.Sleep() 的调用中立即返回,只是增加了内部计数器。

      runtime.write(例如fmt.Println)的所有调用都将以以下标头作为前缀: \0 \0 P B <8-byte time> <4-byte data length> (big endian)

      在这里实现:https://github.com/golang/go/commit/5ff38e476177ce9e67375bd010bea2e030f2fe19

      使用就像go run -tags=faketime test.go一样简单

      示例 test.go:

      package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          fmt.Println("Test!")
          time.Sleep(time.Second * 5)
          fmt.Println("Done.")
      }
      

      输出:

      go run -v -tags=faketime scratch_22.go | hexdump -C
      
      00000000  00 00 50 42 11 74 ef ed  ab 18 60 00 00 00 00 06  |..PB.t....`.....|
      00000010  54 65 73 74 21 0a 00 00  50 42 11 74 ef ee d5 1e  |Test!...PB.t....|
      00000020  52 00 00 00 00 06 44 6f  6e 65 2e 0a              |R.....Done..|
      0000002c
      

      但是,我不建议将其用于实际的单元测试,因为更改为 runtime.write 可能会产生意想不到的后果,破坏很多其他东西。

      【讨论】:

        【解决方案3】:

        我们可以存根时间。现在只需使用 go package "github.com/undefinedlabs/go-mpatch"

        导入 go-mpatch 包并将以下代码 sn-p 放在代码中任何需要存根的地方 time.Now()

         mpatch.PatchMethod(time.Now, func() time.Time {
            return time.Date(2020, 11, 01, 00, 00, 00, 0, time.UTC)
        })
        

        根据需要替换 time.Date 的值。

        签出示例代码以检查 go-mpatch

        的工作情况

        go-playground sample

        【讨论】:

          【解决方案4】:

          在测试代码中有多种方法可以模拟或存根 time.Now():

          • 将时间实例传递给函数
          func CheckEndOfMonth(now time.Time) {
            ...
          }
          
          • 将生成器传递给函数
          CheckEndOfMonth(now func() time.Time) {
              // ...
              x := now()
          }
          
          • 带有接口的摘要
          type Clock interface {
              Now() time.Time
          }
          
          type realClock struct {}
          func (realClock) Now() time.Time { return time.Now() }
          
          func main() {
              CheckEndOfMonth(realClock{})
          }
          
          • 包级时间生成器函数
          type nowFuncT func() time.Time
          
          var nowFunc nowFuncT
          
          func TestCheckEndOfMonth(t *Testing.T) {
              nowFunc = func() time.Time {
                  return time.Now()
              }
              defer function() {
                  nowFunc = time.Now
              }
          
              // Test your code here
          }
          
          • 在结构中嵌入时间生成器
          type TimeValidator struct {
              // .. your fields
              clock func() time.Time
          }
          
          func (t TimeValidator) CheckEndOfMonth()  {
              x := t.now()
              // ...
          }
          
          func (t TimeValidator) now() time.Time  {
              if t.clock == nil {
                  return time.Now() // default implementation which fall back to standard library
              }
          
              return t.clock()
          }
          

          每个都有自己的优点和缺点。最好的办法是将生成时间的函数和使用时间的处理部分分开。

          这篇帖子Stubing Time in golang 详细介绍了它,并且有一个示例可以轻松测试具有时间依赖性的函数。

          【讨论】:

            【解决方案5】:

            如果你需要mock的方法很少,比如Now(),你可以做一个包变量,可以被测试覆盖:

            package foo
            
            import "time"
            
            var Now = time.Now
            
            // The rest of your code...which calls Now() instead of time.Now()
            

            然后在你的测试文件中:

            package foo
            
            import (
                "testing"
                "time"
            )
            
            var Now = func() time.Time { return ... }
            
            // Your tests
            

            【讨论】:

            • 注意:您不必公开现在即可在测试中访问它。
            • 我喜欢这个解决方案;但是var Now = 怎么了?为什么在这里使用函数?
            • @IvanAracki:我不明白你的问题。如果你想存根一个函数,你必须这样做使用一个函数
            • 因为时间不断变化,所以一定是一个函数。
            • 如果您有重复的定义,它只会创建重复定义的错误。不要那样做。并尽可能避免init。这里绝对没有理由使用 init 。在这种情况下,init 所做的只是掩盖您的编码错误,并且可能会引入额外的副作用(因为您已经替换了 Now——任何依赖于旧行为的东西现在都可能被破坏了)。
            【解决方案6】:

            从谷歌结果我找到了比较简单的解决方案:Here

            基本思想是使用另一个函数调用“nowFunc”来获取 time.Now()。 在您的 main 中,初始化此函数以返回 time.Now()。在您的测试中,初始化此函数以返回一个固定的假时间。

            【讨论】:

            • 我也在使用这种方法,但想知道这种方法是否有任何缺点。这样做的好处是它避免了传递Clock 接口。
            【解决方案7】:

            通过实现自定义界面,您已经走在正确的道路上。我认为您使用了您发布的 golang 坚果线程中的以下建议:


            type Clock interface {
              Now() time.Time
              After(d time.Duration) <-chan time.Time
            }
            

            并提供具体的实现

            type realClock struct{}
            func (realClock) Now() time.Time { return time.Now() }
            func (realClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
            

            和一个测试实现。


            Original

            在进行测试(或一般情况下)时更改系统时间是个坏主意。 在执行测试时,您不知道什么取决于系统时间,并且您不想通过花费数天的调试时间来找出困难的方法。只是不要这样做。

            也没有办法在全球范围内隐藏时间包,这样做是不行的 界面解决方案无法实现的更多功能。您可以编写自己的时间包 它使用标准库并提供切换到模拟时间库的功能 测试困扰您的接口解决方案是否是您需要传递的时间对象。

            设计和测试代码的最佳方法可能是使尽可能多的代码无状态。 将您的功能拆分为可测试的无状态部分。单独测试这些组件会容易得多。此外,更少的副作用意味着更容易让代码同时运行。

            【讨论】:

            • @stephanos 您应该将此作为单独的答案发布,因为它很容易错过。回购本身的使用示例也会很有用。
            • 这样做的缺点是,如果用例需要多次,我们需要为每次创建一个结构体。例如 midnightClock 返回午夜的时间,christmass2015Clock 返回特定时间
            【解决方案8】:

            我使用bouk/monkey package 将代码中的time.Now() 调用替换为假的:

            package main
            
            import (
                "fmt"
                "time"
            
                "github.com/bouk/monkey"
            )
            
            func main() {
                wayback := time.Date(1974, time.May, 19, 1, 2, 3, 4, time.UTC)
                patch := monkey.Patch(time.Now, func() time.Time { return wayback })
                defer patch.Unpatch()
                fmt.Printf("It is now %s\n", time.Now())
            }
            

            这在测试中可以很好地伪造系统依赖关系并避免abused DI pattern。生产代码与测试代码保持分离,您可以获得对系统依赖项的有用控制。

            【讨论】:

            • 注意:由于它的许可证github.com/bouk/monkey/blob/master/LICENSE.md,您不应该在 prod 中使用此库
            • 无论许可证如何,您都不应该在生产环境中执行此操作。
            • @AllenLuce 这就是为什么许可证看起来像它的样子?似乎是一个充满敌意的许可
            • @Rambatino 我没有写许可证,所以我不能给你一个真正的答案。有很多与许可证无关的原因将其排除在生产代码之外:它与内联不兼容并且不是线程安全的。并且重新定义库函数可能会使以后必须处理您的烂摊子的任何人感到困惑。但是在测试环境中明智地使用这个包可以使你的生产代码比这个问题的答案中描述的任何其他技术更简单。
            • 我知道这一切——但“我不授予任何人出于任何目的使用此工具的权限”——这是否意味着不要将其用于测试?这只是让我感到困惑,为什么它会包含在一个明确发布供人们使用的包中?
            【解决方案9】:

            此外,如果您只需要存根 time.Now,您可以将依赖项作为函数注入,例如

            func moonPhase(now func() time.Time) {
              if now == nil {
                now = time.Now
              }
            
              // use now()...
            }
            
            // Then dependent code uses just
            moonPhase(nil)
            
            // And tests inject own version
            stubNow := func() time.Time { return time.Unix(1515151515, 0) }
            moonPhase(stubNow)
            

            如果您来自动态语言背景(例如 Ruby),这一切都有些难看:(

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-02-17
              • 1970-01-01
              • 2023-04-03
              • 1970-01-01
              相关资源
              最近更新 更多