【问题标题】:How to test unlikely concurrent scenarios?如何测试不太可能的并发场景?
【发布时间】:2018-02-22 15:07:14
【问题描述】:

例如,像这样访问地图:

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 条件的内容。然而,通过放置断点,在该块中结束是微不足道的。

这个例子不是人为的,因为:

  1. 如果我们跳过RLock,当工作负载主要是读取时,映射将被不必要地锁定。
  2. 如果我们跳过第二个if,最昂贵的工作(在这种情况下由pool.c &lt;- fetchWork{rp, url} 处理)可能会为同一个键发生多次,这是不可接受的。

【问题讨论】:

  • 对于您的第一点,我认为这并不重要;有问题的“工作量”是地图中的单键查找。鉴于此,您可以转储整个第一个(只读)部分 - 从 RLock 到第二个 RUnlock 的所有内容。
  • @Adrian,这意味着每次查找都会Lock(Alone!) 地图。这意味着它不仅会阻止其他此类查找,还会被长时间运行的RLocks 阻止,并阻止未来的RLockers 直到长时间的RLock,然后此处的查找清除。
  • 应用程序是长时间持有锁,还是只是在地图中添加/删除值?
  • @MihailMalostanidis 和?在它目前只有 RLock 的情况下,它只会锁定进行单个地图查找所需的时间 - 几个纳秒。根据第一次检查实际上提前退出的频率,跳过第一次 RLocked 测试并在 Locked 后执行它实际上可能更快。
  • @CeriseLimón 我不能保证其他函数在迭代整个地图时不会持有RLock,或者更糟糕的是,在迭代时同步对某些值进行处理。

标签: unit-testing testing go concurrency code-coverage


【解决方案1】:

我。嘲讽pool.cacheLock.Lock()

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

使用接口模拟

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

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

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

【讨论】:

  • 嵌入的酷用!
  • 我认为第一个选项是最好的,能够在正常使用及其正常方法中使用正常的RWMutex。即使fPool 有构造函数,我也总能进去把假的rwmutex 塞进去。
  • 使用接口而不是具体的RWMutex 是否会影响运行时性能?
  • @MihailMalostanidis 还添加了另一种使用 test 标志的方法。
  • @MihailMalostanidis 可能会有一点点开销,但与 RWMutex 内部发生的所有事情相比,这并不重要。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-27
相关资源
最近更新 更多