有没有一种简单的方法来存根时间.now() 在测试期间全局



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

我刚刚实现了时间接口,

在测试过程中,我使用了时间接口的存根实现,类似于这个 golang-nuts 讨论。

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

有没有另一种方法可以在全球范围内存根time.Now()?也许进行系统调用以更改系统时钟?

我们可以编写自己的时间包,基本上环绕

时间包,但允许我们更改它吗?

我们目前的实现效果很好。我是围棋初学者,我很好奇是否有人有其他想法。

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

<小时 />
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) }

和测试实施。

<小时 />

源语言

在进行测试时(或一般情况下(更改系统时间是一个坏主意。

不知道在执行测试时什么取决于系统时间,并且您不想通过花费数天的调试来找出困难的方法。只是不要这样做。

也没有办法全局阴影时间包,这样做也行不通更多您无法使用接口解决方案完成的事情。您可以编写自己的时间包它使用标准库并提供切换到模拟时间库的功能测试它是否是您需要通过困扰您的接口解决方案传递的时间对象。

设计和测试代码的最佳方法可能是使尽可能多的代码无状态。

将您的功能拆分为可测试的无状态部分。这样单独测试这些组件就容易多了。此外,更少的副作用意味着使代码并发运行要容易得多。

如果你需要模拟的方法很少,比如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"
)
func TestFoo(t *testing.T) {
    now = func() time.Time { return ... }
    // The rest of your test
}

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

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 %sn", time.Now())
}

这在伪造系统依赖项的测试中效果很好,并避免了滥用的依赖项注入 (DI( 模式。生产代码与测试代码保持分离,您可以获得对系统依赖关系的有用控制。

此外,

如果您只需要存根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(,所有这些都有点丑陋:(

有多种方法可以模拟或存根时间。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()
    }
    

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

文章 Stubbing Time in golang 详细介绍了它,并且有一个示例可以方便地测试具有时间依赖性的函数。

这与乔纳森·霍尔的回答相同,但我添加一个具体的例子。

概念:

  • 您可以创建一个名为 CurrentTime 的全局函数来包装time.now()
  • 在测试中重新分配 CurrentTime 函数,并使其返回所需的值。

文件主.go

package main
import (
    "fmt"
    "time"
)
func main() {
    fmt.Printf("This is the current year : %d ", GetCurrentYear())
}
// 'GetCurrentYear' function uses 'CurrentTime' function internally
func GetCurrentYear() int {
    return CurrentTime().Year()
}
var CurrentTime = func() time.Time {
    return time.Now()
}

文件 main_test.go

package main
import (
    "testing"
    "time"
    . "gopkg.in/check.v1"
)
func Test(t *testing.T) { TestingT(t) }
type S struct{}
var _ = Suite(&S{})
func (s *S) TestCurrentYearShouldBeReturnedCorrectly(c *C) {
    expectedYear := 2022
    curentInstant := time.Date(expectedYear, 12, 01, 00, 00, 00, 0, time.UTC)
    // Make 'CurrentTime' return hard-coded time in tests
    CurrentTime = func() time.Time {
        return curentInstant
    }
    c.Assert(GetCurrentYear(), Equals, expectedYear)
}

这是Go Playground链接。

我们可以存根时间。现在只需使用 Go 包 undefinedlabs/go-mpatch

导入 go-mpatch 包,并将以下代码片段放在代码中需要存根时间的任何位置。现在((

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

替换时间的值。日期根据您的需要。

查看示例代码以检查 Go-mpatch 的工作情况。

游乐场示例

我在这里找到了一个相对简单的解决方案。基本思想是使用另一个称为"nowFunc"的函数来获取时间。现在((。

在你的main中,初始化这个函数以返回时间。现在((。在测试中,初始化此函数以返回固定的假时间。

您也可以使用用于Go Playground的faketime方法。它将保留一个内部"时钟"值来替换time.Now(),并且会立即从任何对time.Sleep()的调用中返回,只是增加内部计数器。

runtime.write的所有调用(例如,fmt.Println (都将以以下标头为前缀: 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的更改可能会产生意想不到的后果,破坏许多其他事情。

简单的替代方法是您可以使用sqlmock。AnyArg(( 来打发时间。现在((作为参数。

如果查询是

[sqlBuilder.Update][2](tableName).Set("last_updated", time.Now()).Where(sq.Eq{"id": id}).ToSql()

你想嘲笑这个,做

sqlMock.ExpectExec("UPDATE tableName SET last_updated = ? WHERE id = ?").WithArgs(sqlmock.AnyArg())

而不是

sqlMock.ExpectExec("UPDATE tableName SET last_updated = ? WHERE id = ?").WithArgs(time.Now())

对我有用的是一个小结构

package clock
import "time"
type Clock struct {
    MockTime time.Time
}
func (c Clock) Now() time.Time {
    if c.MockTime.IsZero() {
        return time.Now() // use default golang
    } else {
        return c.MockTime
    }
}

将 Clock 结构作为依赖项嵌入到结构中,或将其作为函数参数传递。

你可以使用 agiledragon/gomonkey,它类似于 bouk/monkey 包。像这样使用:

package foo
import (
    "testing"
    "github.com/agiledragon/gomonkey/v2"
)
func TestAny(t *testing.T) {
    patches := gomonkey.ApplyFunc(time.Now, func() time.Time {
        return time.Unix(1615256178, 0)
    })
    defer patches.Reset()
    ...
}

最新更新