如何有效地测试管道和过滤器模式

How to effectively test the pipes and filters pattern

我正在使用 blog post 中描述的管道和过滤器模式。

我想知道如何有效地测试它。我的想法是独立测试每个过滤器。例如,我有一个看起来像这样的过滤器

func watchTemperature(ctx context.Context, inStream <-chan int) {
    maxTemp = 90

    go func() {
        for {
            select {
            case <-ctx.Done():
                return
            case temp := <-inStream:
                if temp > maxTemp{
                    log.Print("Temperature too high!")
                }
            }
        }
    }()
}

我目前的测试只是想看看日志是否打印出来了。 我的测试如下所示。

func TestWatchTemperature(t *testing.T) {
    maxTemp = 90

    ctx := context.Background()
    inStream := make(chan int)
    defer close(inStream)
    watchTemperature(ctx, inStream)

    var buf bytes.Buffer
    log.SetOutput(&buf)

    inStream<-maxTemp+1

    logMsg := buf.String()
    assert.True(t,  strings.Contains(logMsg, "Temperature too high!"), 
        "Expected log message not found")
}

因为这个过滤器是我管道的末端,所以我没有可以读取的输出通道来确定这个 goroutine/filter 是否已经做了一些事情。

到目前为止,我在网上发现的唯一一件事是,在我的测试中写入 inStream 后等待几秒钟,然后检查日志。然而,这似乎是一个非常糟糕的选择,因为它简单地引入了竞争条件并减慢了测试速度。

测试此类内容的最佳方法是什么,或者根本没有什么好的方法可以用我的过滤器设计来测试它并且我总是需要一个 outStream?

我认为你应该稍微改变一下你的结构。首先,测试一个函数是否打印某些东西对我来说似乎一点都不好。日志不应该成为您业务逻辑的一部分。它们只是使调试和跟踪更容易的附加组件。其次,您正在启动一个不提供任何输出(日志除外)的 goroutine,因此您无法控制它何时完成工作。

另一种解决方案:

声明一个通道以从您的函数获取输出,最好将其传递给您的函数。我使用了一个字符串通道尽可能简单。

var outStream = make(chan string)
watchTemperature(ctx, inStream, outStream)

而不是正常的日志工具,登录到这个频道,你应该为每个输入令牌创建一个输出令牌:

if temp > maxTemp {
    outStream <- "Temperature too high!"
} else {
    outStream <- "Normal"
}

并且在每次发送后的测试中等待输出:

inStream <- maxTemp + 1
reply <- outStream
if reply != "Temperature too high!" {
    // test failed
}

worker goroutine 并不总是有结果要交付。但是,如果您想确切知道它何时完成,则需要使用并发原语之一将其与您的主 goroutine 同步。它可以是信令通道,也可以是等待组。

这是一个例子:

package main

import (
    "bytes"
    "context"
    "fmt"
    "log"
    "strings"
)

const (
    maxTemp = 90
    errMsg  = "Temperature too high!"
)

func watchTemperature(ctx context.Context, inStream <-chan int, finished chan<- bool) {
    go func() {
        defer func() {
            finished <- true
        }()
        for {
            select {
            case <-ctx.Done():
                return
            case temp := <-inStream:
                if temp > maxTemp {
                    log.Print(errMsg)
                }
            }
        }
    }()
}

func main() {
    // quit signals to stop the work
    ctx, quit := context.WithCancel(context.Background())
    var buf bytes.Buffer
    // Make sure, this is called before launching the goroutine!
    log.SetOutput(&buf)
    inStream := make(chan int)
    finished := make(chan bool)
    // pass the callback channel to the goroutine
    watchTemperature(ctx, inStream, finished)

    // asynchronously to prevent a deadlock
    go func() {
        inStream <- maxTemp + 1
        quit()
    }()
    // Block until the goroutine returns.
    <-finished

    if !strings.Contains(buf.String(), errMsg) {
        panic("Expected log message not found")
    }

    fmt.Println("Pass!")
}