前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#86 Sleeping in unit tests

Go语言中常见100问题-#86 Sleeping in unit tests

作者头像
数据小冰
发布2022-08-15 15:31:27
5050
发布2022-08-15 15:31:27
举报
文章被收录于专栏:数据小冰
在单元测试中使用time.Sleep函数

flaky test是一种不可靠的测试现象:即在同样的软件代码和配置环境下,得不到确定(有时成功、有时失败)的测试结果。不确定的测试被认为是测试中的最大的障碍之一,因为它的调试成本很高,并且会破坏我们对测试准确性的信心。在Go语言测试中调用time.Sleep函数可能是一个明显的信号,表明代码可能存在脆弱性。事实上,在测试并发程序的时候使用time.Sleep是相当频繁的. 在本文中,我们可以学习到从测试中删除睡眠(time.Sleep)以防止编写不稳定测试的具体方法。

下面通过一个具体的例子进行说明。程序中定义了一个Handler结构体,结构体包含n和publisher两个字段,通过publisher发布Foo切片的前n元素。该结构体有一个getBestFoo方法,该方法会返回一个Foo对象,并启动一个在后台执行作业的goroutine. 在函数内部实现上,调用getFoos函数获取一个Foo切片,并将切片的第一个元素返回,同时将Foo切片中的前n个元素传给h.publisher的Publish方法。

代码语言:javascript
复制
type Handler struct {
        n         int
        publisher publisher
}

type publisher interface {
        Publish([]Foo)
}

func (h Handler) getBestFoo(someInputs int) Foo {
        foos := getFoos(someInputs)
        best := foos[0]

        go func() {
                if len(foos) > h.n {
                        foos = foos[:h.n]
                }
                h.publisher.Publish(foos)
        }()

        return best
}

我们该如何测试这个功能呢?测试getBestFoo的响应直接通过返回值断言即可判断,但是还想检查传递给Publish的内容怎么办?我们可以通过Mock publisher接口模拟它的行为,然后记录调用Publish方法时传递给它的参数。现在问题来了,在什么时候检查传递给Publish方法的Foo切片呢?因为getBestFoo中启动一个goroutine来执行Publish操作,goroutine调度的时机是无法预知的,所以执行Publish的时间是不确定的,为了防止在检查前还没有执行,一种可能的方法是在检查前休眠几毫秒。

代码语言:javascript
复制
type publisherMock struct {
        mu  sync.RWMutex
        got []Foo
}

func (p *publisherMock) Publish(got []Foo) {
        p.mu.Lock()
        defer p.mu.Unlock()
        p.got = got
}

func (p *publisherMock) Get() []Foo {
        p.mu.RLock()
        defer p.mu.RUnlock()
        return p.got
}

func TestGetBestFoo(t *testing.T) {
        mock := publisherMock{}
        h := Handler{
                publisher: &mock,
                n:         2,
        }

        foo := h.getBestFoo(42)
        // Check foo

        time.Sleep(10 * time.Millisecond)
        published := mock.Get()
        // Check published
}

上面的程序定义了一个publisherMock结构实现了publisher接口,用于模拟Publish行为,该结构中的mu用于保护对字段got访问。在进行单元测试的时候,先调用time.Sleep函数休眠10毫秒,然后再调用mock.Get获取传递给Publish的参数,并对参数进行检查。

严格来说,测试函数TestGetBestFoo执行的结果是不稳定的,不能严格保证延迟10毫秒就可以了(上面程序延迟的是10毫秒,一般来说这个时间足够了,但是还是不能严格保证,因为goroutine调度时机不是我们控制的)。

有哪些方法可以改进上述单元测试呢?第一种方法是采用重试操作,多判断几次。例如,可以编写一个函数,该函数接收有断言函数、最大重试次数和等待时间三个参数,它执行多次检查操作,每次检查完休眠一会。具体实现代码如下:

代码语言:javascript
复制
func assert(t *testing.T, assertion func() bool,
        maxRetry int, waitTime time.Duration) {
        for i := 0; i < maxRetry; i++ {
                if assertion() {
                        return
                }
                time.Sleep(waitTime)
        }
        t.Fail()
}

上述函数中会对断言进行检查,并在重试一定次数后失败。断言函数assert中虽然也在使用time.Sleep, 但是我们可以传递给它更短的等待时间,相比前面的TestGetBestFoo函数,可以缩短等待时间。

代码语言:javascript
复制
assert(t, func() bool {
        return len(mock.Get()) == 2
}, 30, time.Millisecond)

像上面这样,传递给assert函数最大等待时间为1毫秒,并且配置最多尝试重试30次,如果在前10次尝试中测试成功,相比前面休眠10毫秒,会减少执行等待时间。因此,采用重试策略比前面被动休眠更好。

「NOTE:一些测试库(例如testify)也提供重试功能。例如,在testify中,我们可以使用Eventually函数来实现上面的重试等待功能。」

第二种改进方法是采用同步策略,可以使用通道(channel)来同步goroutine。例如,在模拟publisher接口时,将Foo切片数据发送到通道中。下面就是采用channel的改进版本,发布者publisherMock将接收到的数据发送到通道p.ch中,在测试程序TestGetBestFoo中调用publisherMock进行模拟,并根据接收到的值 mock.ch 进行断言。为了确保不会永远等待 mock.ch 问题产生,可以实现一个超时策略,例如,可以在select 中使用 time.After 进行超时保护退出。

代码语言:javascript
复制
type publisherMock struct {
        ch chan []Foo
}

func (p *publisherMock) Publish(got []Foo) {
        p.ch <- got
}

func TestGetBestFoo(t *testing.T) {
        mock := publisherMock{
                ch: make(chan []Foo),
        }
        defer close(mock.ch)

        h := Handler{
                publisher: &mock,
                n:         2,
        }
        foo := h.getBestFoo(42)
        // Check foo

        if v := len(<-mock.ch); v != 2 {
                t.Fatalf("expected 2, got %d", v)
        }
}

上面两种消除测试中的不确定性改进方法,哪种更好呢?是重试还是采用同步方式。一般来说,如果能够同步,采用同步方式是默认选项。实际上,如果设计得当,能够将等待时间约束限制在某个值以内,并且使程序具有完全的确定性。如果不能应用同步方式,我们应该重新考虑自己的设计是否有问题,对于确实不能用同步实现的,应该使用重试方法,无论如何,这也比被动休眠一段时间更好。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 在单元测试中使用time.Sleep函数
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档