unit-testing - 如何测试不太可能发生的并发场景?

标签 unit-testing testing go concurrency code-coverage

例如,这样的 map 访问:

func (pool *fPool) fetch(url string) *ResultPromise {
    pool.cacheLock.RLock()
    if rp, pres := pool.cache[url]; pres {
        pool.cacheLock.RUnlock()
        return rp
    }
    pool.cacheLock.RUnlock()
    pool.cacheLock.Lock()
    if rp, pres := pool.cache[url]; pres {
        pool.cacheLock.Unlock()
        // Skip adding url if someone snuck it in between RUnlock an Lock
        return rp
    }
    rp := newPromise()
    pool.cache[url] = rp
    pool.cacheLock.Unlock()
    pool.c <- fetchWork{rp, url}
    return rp
}

这里是第二个if的内容条件不属于承保范围。然而,通过放置断点,最终进入该 block 是微不足道的。

这个例子并不是故意的,因为:

  1. 如果我们跳过RLock ,当工作负载主要是读取时, map 将被不必要的锁定。
  2. 如果我们跳过第二个 if ,对于同一个 key ,最昂贵的工作(在本例中由 pool.c <- fetchWork{rp, url} 处理)可能会发生多次,这是 Not Acceptable 。

最佳答案

我。模拟pool.cacheLock.Lock()

覆盖该分支的一种方法是模拟pool.cacheLock.Lock(),并且模拟版本可以将url插入到 map 中。所以这次调用后再次检查,会发现执行会进入第二个if语句的主体。

使用接口(interface)进行模拟

模拟pool.cacheLock.Lock()的一种方法是使pool.cacheLock成为一个接口(interface),并且在测试中您可以设置一个模拟值,其 >Lock() 方法将执行“脏插入”到 map 中。

这是使用 pool.cacheLock 接口(interface)的代码的简化版本:

type rwmutex interface {
    Lock()
    RLock()
    RUnlock()
    Unlock()
}

type fPool struct {
    cache     map[string]string
    cacheLock rwmutex
}

func (pool *fPool) fetch(url string) string {
    pool.cacheLock.RLock()
    if rp, pres := pool.cache[url]; pres {
        pool.cacheLock.RUnlock()
        return rp
    }
    pool.cacheLock.RUnlock()
    pool.cacheLock.Lock()
    if rp, pres := pool.cache[url]; pres {
        pool.cacheLock.Unlock()
        // Skip adding url if someone snuck it in between RUnlock an Lock
        return rp
    }
    rp := url + "~data"
    pool.cache[url] = rp
    pool.cacheLock.Unlock()
    return rp
}

它的正常用法是:

pool := fPool{
    cache:     map[string]string{},
    cacheLock: &sync.RWMutex{},
}
fmt.Println(pool.fetch("http://google.com"))

以及一个将触发第二个 if 正文的测试用例:

type testRwmutex struct {
    sync.RWMutex // Embed RWMutex so we don't have to implement everything
    customLock   func()
}

func (trw *testRwmutex) Lock() {
    trw.RWMutex.Lock()
    if trw.customLock != nil {
        trw.customLock()
    }
}

func TestFPoolFetch(t *testing.T) {
    trw := &testRwmutex{RWMutex: sync.RWMutex{}}
    pool := &fPool{
        cache:     map[string]string{},
        cacheLock: trw,
    }

    exp := "http://google.com~test"
    trw.customLock = func() {
        pool.cache["http://google.com"] = exp
    }

    if got := pool.fetch("http://google.com"); got != exp {
        t.Errorf("Expected: %s, got: %s", exp, got)
    }
}

使用函数字段进行模拟

模拟pool.cacheLock.Lock()的另一种方法是将这个功能“外包”到函数类型的字段,测试可以替换为一个函数,除了调用这个函数之外,还可以将其替换为函数。执行“脏插入”。

再次简化示例:

func NewFPool() *fPool {
    mu := &sync.RWMutex{}
    return &fPool{
        cache:     map[string]string{},
        cacheLock: mu,
        lock:      mu.Lock,
    }
}

type fPool struct {
    cache     map[string]string
    cacheLock *sync.RWMutex
    lock      func()
}

func (pool *fPool) fetch(url string) string {
    pool.cacheLock.RLock()
    if rp, pres := pool.cache[url]; pres {
        pool.cacheLock.RUnlock()
        return rp
    }
    pool.cacheLock.RUnlock()
    pool.lock()
    if rp, pres := pool.cache[url]; pres {
        pool.cacheLock.Unlock()
        // Skip adding url if someone snuck it in between RUnlock an Lock
        return rp
    }
    rp := url + "~data"
    pool.cache[url] = rp
    pool.cacheLock.Unlock()
    return rp
}

正常用法是:

pool := NewFPool()
fmt.Println(pool.fetch("http://google.com"))

以及一个将触发第二个 if 正文的测试用例:

func TestFPoolFetch(t *testing.T) {
    pool := NewFPool()
    oldLock := pool.lock

    exp := "http://google.com~test"
    pool.lock = func() {
        oldLock()
        pool.cache["http://google.com"] = exp
    }

    if got := pool.fetch("http://google.com"); got != exp {
        t.Errorf("Expected: %s, got: %s", exp, got)
    }
}

二。使用简单的 test 标志

这里的想法是,为了支持简单的测试,您可以在 fPool 的实现中构建一个简单的 test 标志(例如,它可以是 fPool),并且您要测试的代码会故意检查此标志:

type fPool struct {
    cache     map[string]string
    cacheLock *sync.RWMutex
    test      bool
}

func (pool *fPool) fetch(url string) string {
    pool.cacheLock.RLock()
    if rp, pres := pool.cache[url]; pres {
        pool.cacheLock.RUnlock()
        return rp
    }
    pool.cacheLock.RUnlock()
    pool.cacheLock.Lock()
    if rp, pres := pool.cache[url]; pres || pool.test {
        pool.cacheLock.Unlock()
        // Skip adding url if someone snuck it in between RUnlock an Lock
        return rp
    }
    rp := url + "~data"
    pool.cache[url] = rp
    pool.cacheLock.Unlock()
    return rp
}

现在,如果您想测试第二个 if 的主体,您要做的就是:

func TestFPoolFetch(t *testing.T) {
    pool := NewFPool()
    pool.test = true

    exp := ""
    if got := pool.fetch("http://google.com"); got != exp {
        t.Errorf("Expected: %s, got: %s", exp, got)
    }
}

关于unit-testing - 如何测试不太可能发生的并发场景?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48930732/

相关文章:

java - 测试 Jersey 客户端包装器

java - PowerMock和EasyMock方法模拟问题

python - 如何测试包含 datetime.now() 的不同日期的 python 方法?

android - Gradle Android 找不到方法 testPackage()

android - 如何在 Android 中填充测试数据库?

debugging - 远程调试 Gogland 停止

c# - ASP.NET Web API 2 中的 AuthorizeAttribute 发生了哪些变化?

testing - 试图将我们的手动测试计划自动填充到 TFS 中的每个构建/迭代中?

go - 设置 %s 从字节编码的十六进制到变量 golang

go - 如何将接口(interface)转换为接口(interface) slice ?