在测试期间,是否有一种简单的方法可以全局替换time.Now()函数?

huangapple go评论92阅读模式
英文:

Is there an easy way to stub out time.Now() globally during test?

问题

我们的代码中有一部分是时间敏感的,我们需要能够预留某个时间段,然后在30-60秒后释放它,我们可以使用time.Sleep(60 * time.Second)来实现。

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

然而,time.Now()在多个地方被调用,这意味着我们需要传递一个变量来跟踪我们实际上睡眠了多长时间。

有没有其他方法可以全局替换time.Now()的存根?也许可以通过系统调用来改变系统时钟?

我们可以编写自己的时间包,它基本上是在时间包的基础上进行封装,但允许我们进行更改吗?

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

英文:

Part of our code is time-sensitive, and we need to able to reserve something and then release it in 30-60 seconds, etc., which we can just do a time.Sleep(60 * time.Second).

I have just implemented the time interface, and during the test I used a stubbed implementation of the time interface, similar to this golang-nuts discussion.

However, time.Now() is called in multiple sites which means we need to pass a variable around to keep track of how much time we have actually slept.

Is there an alternative way to stub out time.Now() globally? Maybe making a system call to change the system clock?

Can we maybe write our own time package which basically wraps around the time package but allows us to change it?

Our current implementation works well. I am a Go beginner, and I am curious to see if anyone has other ideas.

答案1

得分: 90

通过实现自定义接口,你已经走在了正确的道路上。我理解你正在使用你发布的 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) }

// 提供一个测试实现

原文

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

你不知道在执行测试时依赖于系统时间的内容,你不想通过花费数天进行调试来找到答案。所以最好不要这样做。

也没有办法全局屏蔽 time 包,而且即使这样做也不能做到比接口解决方案更多的事情。你可以编写自己的 time 包,它使用标准库并提供一个函数来切换到一个模拟时间库进行测试,如果你所关心的是需要通过接口解决方案传递的时间对象。

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

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

英文:

With implementing a custom interface you are already on the right way. I take it you use the following advise from the golang-nuts thread you've posted:


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

> and provide a concrete implementation

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

> and a testing implementation.


<sup>Original</sup>

Changing the system time while making tests (or in general) is a bad idea.

You don't know what depends on the system time while executing tests and you don't want to find out the hard way by spending days of debugging into that. Just don't do it.

There is also no way to shadow the time package globally and doing that would not do
anything more you couldn't do with the interface solution. You can write your own time package
which uses the standard library and provides a function to switch to a mock time library for
testing if it is the time object you need to pass around with the interface solution that is bothering you.

The best way to design and test your code would probably be to make as much code stateless as possible.

Split your functionality in testable, stateless parts. Testing these components separately is much easier then. Also, fewer side effects means that it is much easier to make the code run concurrently.

答案2

得分: 53

如果你需要模拟的方法很少,比如Now(),你可以创建一个包变量,在测试中可以被重写:

package foo

import "time"

var now = time.Now

// 其余的代码...调用 now() 而不是 time.Now()

然后在你的测试文件中:

package foo

import (
    "testing"
    "time"
)

func TestFoo(t *testing.T) {
    now = func() time.Time { return ... }
    // 其余的测试代码
}
英文:

If the methods you need to mock are few, such as Now(), you can make a package variable which can be overwritten by tests:

package foo

import &quot;time&quot;

var now = time.Now

// The rest of your code...which calls now() instead of time.Now()

then in your test file:

package foo

import (
    &quot;testing&quot;
    &quot;time&quot;
)

func TestFoo(t *testing.T) {
    now = func() time.Time { return ... }
    // The rest of your test
}

答案3

得分: 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("现在的时间是 %s\n", time.Now())
}

这在测试中很有效,可以伪造系统依赖,避免了滥用依赖注入(DI)模式。生产代码与测试代码保持分离,同时可以对系统依赖进行有用的控制。

英文:

I use the bouk/monkey package to replace the time.Now() calls in my code with a fake:

package main

import (
    &quot;fmt&quot;
    &quot;time&quot;

    &quot;github.com/bouk/monkey&quot;
)

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(&quot;It is now %s\n&quot;, time.Now())
}

This works well in tests to fake out system dependencies and avoids the abused dependency injection (DI) pattern. Production code stays separate from test code and you gain useful control of system dependencies.

答案4

得分: 10

如果你只需要对time.Now进行存根处理,你可以将其作为一个函数注入依赖,例如:

func moonPhase(now func() time.Time) {
  if now == nil {
    now = time.Now
  }

  // 使用 now()...
}

// 然后依赖的代码只需使用
moonPhase(nil)

// 测试时注入自己的版本
stubNow := func() time.Time { return time.Unix(1515151515, 0) }
moonPhase(stubNow)

如果你来自动态语言(如Ruby)的背景,这些代码可能看起来有点丑陋 在测试期间,是否有一种简单的方法可以全局替换time.Now()函数?

英文:

Also if you need to just stub time.Now you can inject the dependency as a function, e.g.,

<!-- language: go -->

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)

Granted, all that is a bit ugly if you come from a dynamic languages background (e.g., Ruby) 在测试期间,是否有一种简单的方法可以全局替换time.Now()函数?

答案5

得分: 7

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

  • 将time的实例传递给函数

    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
        }
    
        // 在这里测试你的代码
    }
    
  • 在结构体中嵌入时间生成器

    type TimeValidator struct {
        // .. 你的字段
        clock func() time.Time
    }
    
    func (t TimeValidator) CheckEndOfMonth()  {
        x := t.now()
        // ...
    }
    
    func (t TimeValidator) now() time.Time  {
        if t.clock == nil {
            return time.Now() // 默认实现,回退到标准库
        }
    
        return t.clock()
    }
    

每种方法都有其优缺点。最好的方法是将生成时间的函数与使用时间的处理部分分离。

文章Stubbing Time in golang详细介绍了这个问题,并提供了一个示例,展示如何使具有时间依赖性的函数易于测试。

英文:

There are multiple way to mock or stub time.Now() in test code:

  • Passing an instance of time to the function

    func CheckEndOfMonth(now time.Time) {
      ...
    }
    
  • Passing a generator to the function

    CheckEndOfMonth(now func() time.Time) {
        // ...
        x := now()
    }
    
  • Abstract with an interface

    type Clock interface {
        Now() time.Time
    }
    
    type realClock struct {}
    func (realClock) Now() time.Time { return time.Now() }
    
    func main() {
        CheckEndOfMonth(realClock{})
    }
    
  • Package level time generator function

    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
    }
    
  • Embed time generator in struct

    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()
    }
    

Each has its own pluses and minuses. The best way is to separate the function that generates the time and the processing part that uses the time.

The post Stubbing Time in golang goes into details about it and there is an example for making function with time dependency to be easily tested.

答案6

得分: 5

这与Jonathan Hall的答案相同,但我添加了一个具体的示例。

概念:

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

文件 main.go

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Printf("当前年份为:%d ", GetCurrentYear())
}

// 'GetCurrentYear' 函数在内部使用 'CurrentTime' 函数
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)

    // 在测试中使 'CurrentTime' 返回硬编码的时间
    CurrentTime = func() time.Time {
        return curentInstant
    }

    c.Assert(GetCurrentYear(), Equals, expectedYear)

}

这里Go Playground的链接。

英文:

This is the same as Jonathan Hall's answer, but I am adding a concrete example.

Concept:

  • You can create a global function called CurrentTime to wrap the time.now()
  • Reassign the CurrentTime function in tests, and make it return the desired value.

File main.go

package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

func main() {
    fmt.Printf(&quot;This is the current year : %d &quot;, GetCurrentYear())
}

// &#39;GetCurrentYear&#39; function uses &#39;CurrentTime&#39; function internally
func GetCurrentYear() int {
    return CurrentTime().Year()
}

var CurrentTime = func() time.Time {
    return time.Now()
}

File main_test.go

package main

import (
    &quot;testing&quot;
    &quot;time&quot;
    . &quot;gopkg.in/check.v1&quot;
)

func Test(t *testing.T) { TestingT(t) }

type S struct{}

var _ = Suite(&amp;S{})

func (s *S) TestCurrentYearShouldBeReturnedCorrectly(c *C) {
    expectedYear := 2022
    curentInstant := time.Date(expectedYear, 12, 01, 00, 00, 00, 0, time.UTC)

    // Make &#39;CurrentTime&#39; return hard-coded time in tests
    CurrentTime = func() time.Time {
        return curentInstant
    }

    c.Assert(GetCurrentYear(), Equals, expectedYear)

}

Here is the Go Playground link.

答案7

得分: 4

我们可以使用Go包undefinedlabs/go-mpatch来简单地对time.Now进行存根。

导入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的工作原理。

go-playground示例

英文:

We can stub time.Now simply by using the Go package undefinedlabs/go-mpatch.

Import the go-mpatch package and put the below code snippet in the code wherever you need to stub time.Now():

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

Replace the values of time.Date as per your need.

check out the sample code to check the working of go-mpatch.

go-playground sample

答案8

得分: 3

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

在你的主函数中,初始化这个函数以返回time.Now()的时间。在你的测试中,初始化这个函数以返回一个固定的虚拟时间。

英文:

I found a relatively simple solution here. The basic idea is using another function called "nowFunc" to get the time.Now().

In your main, initialize this function to return time.Now(). In your test, initialize this function to return a fixed fake time.

答案9

得分: 2

你也可以使用在Go Playground中使用的faketime方法。
它会保持一个内部的“时钟”值,替换time.Now(),并且会立即返回任何对time.Sleep()的调用,仅仅增加内部计数器。

对于所有对runtime.write的调用(例如fmt.Println),都会在前面加上以下标头:
\0 \0 P B <8字节时间> <4字节数据长度>(大端序)

它在这里实现: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的更改可能会产生意想不到的后果,破坏很多其他东西。

英文:

You can also use the faketime method used for Go Playground.
It will keep an internal "clock" value which replaces time.Now(), and will instantly return from any call to time.Sleep(), merely increasing the internal counter.

All calls to runtime.write (for example, fmt.Println) will be prefixed with the following header:
\0 \0 P B &lt;8-byte time&gt; &lt;4-byte data length&gt; (big endian)

It was implemented here: https://github.com/golang/go/commit/5ff38e476177ce9e67375bd010bea2e030f2fe19

Using it is as simple as go run -tags=faketime test.go

Example test.go:

package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
)

func main() {
    fmt.Println(&quot;Test!&quot;)
    time.Sleep(time.Second * 5)
    fmt.Println(&quot;Done.&quot;)
}

Output:

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

However, I wouldn't recommend using this for actual unit tests, as the change to runtime.write will probably have unintended consequences, breaking a lot of other things.

答案10

得分: 0

简单的替代方法是使用sqlmock.AnyArg()将**time.Now()**作为参数传递。

示例

如果查询是

[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())
英文:

The simple alternative is you can use sqlmock.AnyArg() to pass time.Now() as an argument.

Example

If the query is

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

and you want to mock this, do

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

instead of

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

答案11

得分: 0

对我来说有效的是一个小的结构体

package clock

import "time"

type Clock struct {
	MockTime time.Time
}

func (c Clock) Now() time.Time {
	if c.MockTime.IsZero() {
		return time.Now() // 使用默认的golang时间
	} else {
		return c.MockTime
	}
}

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

英文:

What works for me is a small struct

package clock

import &quot;time&quot;

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
	}
}

Embed the Clock struct in your struct as a dependency, or pass it along as function parameter.

答案12

得分: 0

你可以使用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()
    ...
}
英文:

You can use agiledragon/gomonkey, it's similar to bouk/monkey package. Use like this:

package foo

import (
    &quot;testing&quot;
    &quot;github.com/agiledragon/gomonkey/v2&quot;
)

func TestAny(t *testing.T) {
    patches := gomonkey.ApplyFunc(time.Now, func() time.Time {
	    return time.Unix(1615256178, 0)
    })
    defer patches.Reset()
    ...
}

huangapple
  • 本文由 发表于 2013年9月24日 06:58:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/18970265.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定