71

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

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

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

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

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

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

4

10 回答 10

74

通过实现自定义界面,您已经走在了正确的道路上。我认为您使用了您发布的 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) }

和一个测试实现。


原来的

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

也没有办法在全球范围内隐藏时间包,这样做不会做任何你不能用接口解决方案做的事情。您可以编写自己的时间包,该包使用标准库并提供切换到模拟时间库的功能以测试是否是您需要通过接口解决方案传递的时间对象。

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

于 2013-09-23T23:07:46.830 回答
39

如果您需要模拟的方法很少,例如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
于 2017-01-15T13:13:06.010 回答
26

我使用bouk/monkey 包将代码中的调用替换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())
}

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

于 2016-11-16T18:46:34.940 回答
8

此外,如果您只需要存根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),那么这一切都有些难看:(

于 2014-09-11T15:35:31.567 回答
5

在测试代​​码中有多种模拟或存根 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()
}

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

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

于 2019-09-28T09:26:35.377 回答
3

从谷歌结果我找到了相对简单的解决方案:这里

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

于 2017-03-17T00:25:08.647 回答
1

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

runtime.write所有对(f.ex. ) 的调用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可能会产生意想不到的后果,破坏很多其他事情。

于 2021-10-18T12:34:25.990 回答
1

如果你不介意额外的依赖,Go 中有一个易于使用的时钟库来模拟时间,它包装了标准库,因此可以在测试中轻松地模拟它。

于 2021-11-22T07:27:30.903 回答
0

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

导入go-mpatch包并将下面的代码片段放在代码中任何需要存根的地方 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的工作情况

去游乐场样本

于 2021-03-11T16:36:34.587 回答
0

与 Flimsy 的回答相同。添加一个具体的例子。

概念:

  • 您可以创建一个调用CurrentTime来包装的全局函数time.now()
  • 在测试中重新分配CurrentTime函数并使其返回所需的值。
// main.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 链接

于 2022-02-20T09:59:06.390 回答