duanlidi1051
duanlidi1051
2014-02-22 14:03
浏览 101
已采纳

测试使用time.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 >= 0; remaining -= interval {
        tickCallback(remaining)
        <-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!

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 邀请回答

2条回答 默认 最新

  • doucao1888
    doucao1888 2014-02-22 16:07
    已采纳

    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 >= 0; remaining -= ticker.Duration() {
                remainingCh <- 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("%v to go", d)
        }
    }
    

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

    点赞 评论
  • dongzanghui4624
    dongzanghui4624 2014-02-22 16:07

    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("%v to go", 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
    }
    
    点赞 评论

相关推荐