使用time.Ticker测试代码的正确方法是什么?

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

Correct way to test code that uses time.Ticker?

问题

我很乐意为你提供关于如何测试使用time.Ticker的代码的正确方法的建议。

例如,假设我有一个倒计时器,如下所示(这只是我为了提问而想出的一个示例):

type TickFunc func(d time.Duration)

func Countdown(duration time.Duration, interval time.Duration, tickCallback TickFunc) {
    ticker := time.NewTicker(interval)
    for remaining := duration; remaining >= 0; remaining -= interval {
        tickCallback(remaining)
        <-ticker.C
    }

    ticker.Stop()
}

如果我想要对此进行测试,我希望提供一个模拟对象,以便能够快速且可预测地运行测试,因此我需要找到一种方法将我的模拟对象传递给Countdown函数。

我可以想到几种方法来实现这一点:

  1. 创建一个Ticker接口和一个包内部的一级函数,供测试时进行修补:链接

  2. 创建一个Ticker接口,并直接将其实现传递给Countdown函数:链接

如果我选择后一种方式,是否会透露太多关于Countdown如何工作的信息,从而使潜在的客户在使用此代码时更加困难?因为他们需要构造并传递一个Ticker对象,而不是提供持续时间和间隔。

我非常想听听在测试这样的代码时,最佳的方法是什么?或者你会如何更改代码以保留其行为,同时使其更易于测试?

谢谢你的帮助!

英文:

I'd like your advice on the correct way to test code that uses time.Ticker

For instance, let's say I have a countdown timer like below (just an example I thought up for the purposes of this question):

type TickFunc func(d time.Duration)

func Countdown(duration time.Duration, interval time.Duration, tickCallback TickFunc) {
    ticker := time.NewTicker(interval)
    for remaining := duration; remaining &gt;= 0; remaining -= interval {
	    tickCallback(remaining)
	    &lt;-ticker.C
    }

    ticker.Stop()
 }

http://play.golang.org/p/WJisY52a5L

If I wanted to test this, I'd want to provide a mock so that I can have tests that run quickly and predictably, so I'd need to find a way to get my mock into the Countdown function.

I can think of a few ways to do this:

Create a Ticker interface and a first class function internal to the package that I can patch for the purposes of testing: http://play.golang.org/p/oSGY75vl0U

Create a Ticker interface and pass an implementation directly to the Countdown function:
http://play.golang.org/p/i67Ko5t4qk

If I do it the latter way, am I revealing too much information about how Countdown works and making it more difficult for potential clients to use this code? Instead of giving a duration and interval, they have to construct and pass in a Ticker.

I'm very interested in hearing what's the best approach when testing code like this? Or how you would change the code to preserve the behaviour, but make it more testable?

Thanks for your help!

答案1

得分: 4

由于这是一个相当简单的函数,我假设你只是将其作为模拟非平凡内容的示例。如果你真的想测试这段代码,而不是模拟 ticker,为什么不使用非常小的时间间隔呢?

在我看来,第二个选项是两者中更好的选择,让用户调用:

foo(dur, NewTicker(interval)... 

似乎不是太麻烦。

此外,在 Go 中,回调函数是严重的代码异味:

func Countdown(ticker Ticker, duration time.Duration) chan time.Duration {
	remainingCh := make(chan time.Duration, 1)
	go func(ticker Ticker, dur time.Duration, remainingCh chan time.Duration) {
		for remaining := duration; remaining >= 0; remaining -= ticker.Duration() {
			remainingCh <- remaining
			ticker.Tick()
		}
		ticker.Stop()
		close(remainingCh)
	}(ticker, duration, remainingCh)
	return remainingCh
}

然后你可以像这样使用这段代码:

func main() {
	for d := range Countdown(NewTicker(time.Second), time.Minute) {
		log.Printf("%v to go", d)
	}
}

这是在 playground 上的链接:http://play.golang.org/p/US0psGOvvt

英文:

Since this is a pretty simple function, I assume you are just using this as an example of how to mock non-trivial stuff. If you actually wanted to test this code, rather than mocking up ticker, why not just use really small intervals.

IMHO the 2nd option is the better of the two, making a user call:

foo(dur, NewTicker(interval)... 

doesn't seem like much of a burden.

Also having the callback is serious code smell in Go:

func Countdown(ticker Ticker, duration time.Duration) chan time.Duration {
	remainingCh := make(chan time.Duration, 1)
	go func(ticker Ticker, dur time.Duration, remainingCh chan time.Duration) {
		for remaining := duration; remaining &gt;= 0; remaining -= ticker.Duration() {
			remainingCh &lt;- remaining
			ticker.Tick()
		}
		ticker.Stop()
		close(remainingCh)
	}(ticker, duration, remainingCh)
	return remainingCh
}

You could then use this code like:

func main() {
	for d := range Countdown(NewTicker(time.Second), time.Minute) {
		log.Printf(&quot;%v to go&quot;, d)
	}
}

Here it is on the playground: http://play.golang.org/p/US0psGOvvt

答案2

得分: 0

这不回答如何注入模拟部分的问题,但看起来你努力过头了。
如果示例代表了你实际测试的内容,那么只需使用小的数字即可。

http://play.golang.org/p/b_1kqyIu-u

Countdown(5, 1, func(d time.Duration) {
    log.Printf("%v to go", d)
})

现在,如果你正在测试调用 Countdown 的代码(而不是测试 Countdown 本身),那么我可能会创建一个标志,你可以为你的模块设置该标志,以便在相同的调用计数下尽可能快地缩放数字。

http://play.golang.org/p/KqCGnaR3vc

if testMode {
duration = duration/interval
interval = 1
}

英文:

This doesn't answer the how to inject the mock part, but it seems like you are trying too hard.
if the example is representative of what you actually are testing, then just use small numbers.

http://play.golang.org/p/b_1kqyIu-u

Countdown(5, 1, func(d time.Duration) {
		log.Printf(&quot;%v to go&quot;, d)
	})

Now, if you are testing code that calls Countdown (rather than testing Countdown), then I'd probably just create a flag you can set for your module that scales the numbers to be as fast possible with the same invocation count.

http://play.golang.org/p/KqCGnaR3vc

if testMode {
	duration = duration/interval
	interval = 1
}

huangapple
  • 本文由 发表于 2014年2月22日 22:03:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/21955345.html
匿名

发表评论

匿名网友

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

确定