使用包全局变量时,在 Go 测试之间存在竞态条件。

huangapple go评论135阅读模式
英文:

Race condition between Go tests when using package global

问题

我使用一个包全局变量来保存由包中的函数使用的http客户端。这样,它可以被一个用于单元测试的模拟http客户端替换。

每个测试函数创建一个合适的模拟http客户端实例,并在测试主体的其余部分之前将其分配给包全局变量。

测试主体执行启动引用全局变量的长期运行goroutine的代码。当测试函数完成时,这个goroutine仍然在运行。

go测试框架启动了接下来的测试函数,遵循相同的模式。当第一个goroutine读取刚刚被写入第二个函数的模拟http客户端实例时,就会检测到竞争条件。

这是因为测试框架在同一个进程中运行第二个测试函数,因此长时间运行的goroutine不会被终止。

有哪些解决方案可供选择?我非常希望不要为了改进测试性的这个特定方面而向框架添加“停止”语义。

英文:

I use a package global variable to hold the http client to be used by functions in the package. This is so that it can be replaced by a mock http client for unit tests.

Each test function creates an appropriate mock http client instance and assigns it to the package global before the rest of the test body.

The test body exercises code that starts a long-lived goroutine that references the global. This goroutine is still running when the test function is complete.

The go testing framework starts the next test function which follows the same pattern. A race condition is detected when the first goroutine reads the global which has just been written to the second function's mock http client instance.

This arises because the test framework runs the second test function in the same process as the first, and so the long-running goroutine is not killed.

What solutions are available? I would very much prefer not to add "stop" semantics to the framework with the long-running goroutine just to improve this specific aspect of testability.

答案1

得分: 1

你描述的情况是在为引用包全局状态的长寿命goroutine编写测试时常见的挑战。虽然在同一进程中完全避免测试函数之间共享状态可能会有挑战,但有一些策略可以用来解决这个问题,而不需要改变框架或引入"停止"语义:

使用测试设置和拆卸函数: 而不是直接修改包全局变量,您可以使用测试包提供的**TestMain函数来为测试设置和拆卸共享资源。在运行任何测试之前,您可以在TestMain**函数中创建并分配模拟HTTP客户端实例,确保每个测试都从一个干净的状态开始。

创建一个包装器来管理全局变量: 在函数中包装包全局变量,以管理其状态和分配。这可以帮助您更好地控制对全局变量的访问和分配。例如:

var httpClientMu sync.Mutex
var httpClient *http.Client

func SetHTTPClient(client *http.Client) {
    httpClientMu.Lock()
    defer httpClientMu.Unlock()
    httpClient = client
}

func GetHTTPClient() *http.Client {
    httpClientMu.Lock()
    defer httpClientMu.Unlock()
    return httpClient
}

然后,在您的测试中,使用**SetHTTPClient来设置模拟HTTP客户端,并使用GetHTTPClient**在您的代码中访问全局变量。这种方法添加了同步级别,以防止并发访问问题。

使用依赖注入方法: 而不是依赖包全局状态,重构您的代码以将HTTP客户端作为相关函数或方法的参数。这样可以使代码更具可测试性,并消除包全局变量的需要。

考虑上下文: 如果长寿命的goroutine正在进行HTTP请求,请考虑将**context.Context传递给您的函数和goroutine。这允许您控制goroutine的生命周期,并在测试结束时取消它。使用context**包可以帮助您优雅地管理长寿命操作。

尽管这些方法可能需要对您的代码进行一些调整,但它们可以帮助提高可测试性并避免测试函数之间的共享状态问题。在在您的测试框架的限制内工作时,保持代码质量和提高可测试性之间的平衡是很重要的。

英文:

The situation you've described is a common challenge when writing tests for long-lived goroutines that reference package-global state. While it might be challenging to completely avoid sharing state between test functions in the same process, there are some strategies you can use to address this issue without altering the framework or introducing "stop" semantics:

Use Test Setup and Teardown Functions: Instead of directly modifying the package global variable, you can use the TestMain function provided by the testing package to set up and tear down shared resources for your tests. You can create and assign the mock HTTP client instance in the TestMain function before running any tests, ensuring that each test starts with a clean slate.

Create a Wrapper Around the Global Variable: Wrap the package-global variable in a function that manages its state and assignment. This can help you better control access and assignment of the global variable. For example:

var httpClientMu sync.Mutex
var httpClient *http.Client

func SetHTTPClient(client *http.Client) {
    httpClientMu.Lock()
    defer httpClientMu.Unlock()
    httpClient = client
}

func GetHTTPClient() *http.Client {
    httpClientMu.Lock()
    defer httpClientMu.Unlock()
    return httpClient
}

Then, in your tests, use SetHTTPClient to set the mock HTTP client, and use GetHTTPClient to access the global variable in your code. This approach adds a level of synchronization to prevent concurrent access issues.

Use a Dependency Injection Approach: Rather than relying on package-global state, refactor your code to accept the HTTP client as a parameter to the relevant functions or methods. This makes the code more testable and eliminates the need for the package-global variable.

Consider Context: If the long-lived goroutine is making HTTP requests, consider passing a context.Context to your functions and goroutines. This allows you to control the lifecycle of the goroutine and cancel it when the test ends. Using the context package can help manage long-lived operations gracefully.

While these approaches may require some adjustments to your code, they can help improve testability and avoid issues with shared state between test functions. It's important to strike a balance between maintaining code quality and improving testability while working within the constraints of your testing framework.

huangapple
  • 本文由 发表于 2023年8月9日 18:17:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/76866784.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定