Golang依赖注入与如何实现100%测试覆盖率
Golang依赖注入与如何实现100%测试覆盖率 大家好,有一个关于依赖注入和100%测试覆盖率的问题。
假设你有一个函数,它本身调用了某些外部包中的函数,而你绝对不希望在测试中调用这些函数。为了方便起见,我们称它们为“邪恶”函数。 这个外部包没有为这些“邪恶”函数提供任何结构体或接口。 因此,你必须创建自己的接口,其方法作为这些“邪恶”函数的包装器。 然后,你构建这个接口的两个实现:一个用于正常构建,它实际上包装了那些“邪恶”函数;另一个是用于测试的模拟版本,它不会造成任何危害。
到目前为止,这似乎是常规做法(至少根据我目前所读到的内容), 但是测试覆盖率呢?你可以在使用该接口的方法上实现100%的测试覆盖率, 但你无法在用于实际构建的接口实现上达到100%,因为它们调用了那些该死的“邪恶”函数。你根本无法测试那些函数。遗憾的是,我读过的所有博客文章都没有涉及这个问题。
你们是如何处理这类情况的?
更多关于Golang依赖注入与如何实现100%测试覆盖率的实战教程也可以访问 https://www.itying.com/category-94-b0.html
嘿,Stereodude!
我也曾为同样的问题而困扰。我很好奇,是什么让这些函数变得“邪恶”?在我的案例中,这些“邪恶”函数连接的是没有“沙盒”环境的第三方服务,因此除非是在生产构建中,否则调用它们是不好的。
我最终尽我所能地按照你上面描述的方法包装了这些函数,但到某个阶段,我无法再进一步包装和测试了。我试图让未测试的代码尽可能少,但最终我不得不停下来,留下一些未经测试的代码。
我不确定是否有办法解决这个问题,或者这只是这些“邪恶”函数固有的问题。
更多关于Golang依赖注入与如何实现100%测试覆盖率的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
它们并非真的邪恶,只是在您提到的这类单元测试中不受欢迎。我用这个术语来区分可测试和不可测试的函数,但这可能有点误导人 😅。
在我的具体案例中,这些是来自 chromedp 包的函数,它们会启动 Chrome 进程,而我不希望在单元测试中发生这种情况。
我想到的一个可能但非常不优雅的解决方案是,将包装器实现放到一个单独的包中,并在包模式下使用 go test,同时列出除该包之外的所有其他包。但这并不太方便。理想情况下,我希望 go test ./... 能覆盖我 100% 的代码。
哈哈,我明白了 😄
嗯,是的,这看起来像是一个变通方案。我研究了一下如何跳过那些你不想运行的测试,发现这篇 Stack Overflow 帖子似乎很有帮助。
看起来你可以结合使用环境变量和 testing.Skip() 函数来跳过这些测试,或者使用 testing -short 标志。
这样一来,你仍然可以运行 go test ./...,它会忽略你不想运行的测试,除非你特意更改了环境变量或提供了 -short 标志。
根据这篇博客所述,如果你正在测试一个包,那么跨包的内容本身不会被包含在覆盖率统计中。
另一个可以考虑的方向(如果你还没试过的话)是构建模式。例如“plugin”模式。
另一个方面是你如何实现依赖注入。如果你能解释一下你是如何实现依赖注入的,那么对其进行调整或许能找到一个有效的解决方案。例如:我可以动态生成我的“import”语句(编译时依赖注入)。我认为,无法被导入的内容自然也就无法被计入覆盖率。
只是分享一下研究过的选项。如果由于我对你的问题缺乏更深入的理解而导致这些都不适用,我表示歉意,但我很好奇!
感谢你的意见 @connerj70 和 @ArjunDhar! 抱歉我之前没有包含任何示例代码,因为我觉得描述这个问题可能有点冗长,但让我试试看(或多或少是伪代码):
假设你想测试 someMethod():
type SomeStruct struct {}
func (s *SomeStruct) SomeMethod() {
doSomeThings()
someimport.DoThingsYouDontWantInTests()
doSomeOtherThings()
}
为了让 someimport.DoThingsYouDontWantInTests() 在测试中不被执行,你创建了一个接口和一个包装这些函数的实现:
type Wrapper interface {
DoThingsYouDontWantInTests()
}
type ImplementedWrapper struct {}
func (w *ImplementedWrapper) DoThingsYouDontWantInTests() {
someimport.DoThingsYouDontWantInTests()
}
type SomeStruct struct {
w Wrapper
}
func (s *SomeStruct) SomeMethod() {
doSomeThings()
w.DoThingsYouDontWantInTests()
doSomeOtherThings()
}
这样在测试中,你可以对 SomeStruct.w 进行依赖注入:
type MockedWrapper struct {}
func (w *MockedWrapper) DoThingsYouDontWantInTests() {
fmt.Println("I don't do anything")
}
func TestSomeStruct(t *testing.T) {
s := SomeStruct{w: &MockedWrapper{}}
s.SomeMethod()
// 在这里进行断言
}
当然,实际的模拟会使用某种模拟工具,所以会有点不同。
问题在于 func (w *ImplementedWrapper) DoThingsYouDontWantInTests() 无法被测试,导致这部分代码未被覆盖。我希望这能稍微说明我的意思。
@connerj70 你的意思是我可以编写一些测试,这些测试总是会被跳过,但会算入覆盖率吗? @ArjunDhar 很有趣的阅读!你介意详细解释一下如何实现编译时导入吗?你是为此使用构建标签吗?
与此同时,我发现 go test -cover 不会计算不包含任何测试文件的包的覆盖率。所以,测试了一堆包,而留下一个包(仅包含像 func (w *ImplementedWrapper) DoThingsYouDontWantInTests() 这样的包装函数)没有测试文件,不会对代码覆盖率产生负面影响。这实际上就是正确的做法吗?
在Go中实现依赖注入并追求100%测试覆盖率时,确实会遇到外部依赖难以完全覆盖的情况。以下是具体解决方案和示例代码:
1. 定义接口和实现
首先创建接口来抽象外部依赖,并提供生产实现和测试模拟实现。
// 定义接口
type EvilService interface {
EvilFunction() error
}
// 生产实现(包装"邪恶"函数)
type RealEvilService struct{}
func (s *RealEvilService) EvilFunction() error {
// 调用外部包的实际函数
return external.EvilFunction()
}
// 测试模拟实现
type MockEvilService struct{}
func (m *MockEvilService) EvilFunction() error {
// 模拟行为,不调用实际函数
return nil
}
2. 业务逻辑使用依赖注入
type MyService struct {
evilService EvilService
}
func NewMyService(es EvilService) *MyService {
return &MyService{evilService: es}
}
func (s *MyService) Process() error {
// 使用注入的依赖
return s.evilService.EvilFunction()
}
3. 测试业务逻辑(达到100%覆盖率)
func TestMyService_Process(t *testing.T) {
mockService := &MockEvilService{}
service := NewMyService(mockService)
err := service.Process()
if err != nil {
t.Errorf("expected no error, got %v", err)
}
}
4. 处理生产实现的测试覆盖
对于生产实现(RealEvilService),可以采用以下策略:
方案A:使用构建标签隔离测试
// real_evil_service.go
// +build !test
package mypackage
type RealEvilService struct{}
func (s *RealEvilService) EvilFunction() error {
return external.EvilFunction()
}
// real_evil_service_test.go
// +build test
package mypackage
type RealEvilService struct{}
func (s *RealEvilService) EvilFunction() error {
// 测试版本,返回可控结果
return nil
}
方案B:使用接口适配器测试核心逻辑
// 测试适配器实现
func TestRealEvilService_Integration(t *testing.T) {
if testing.Short() {
t.Skip("跳过集成测试")
}
service := &RealEvilService{}
err := service.EvilFunction()
// 验证函数签名和行为,不验证具体实现
_ = err // 根据实际情况添加断言
}
方案C:使用可配置的包装器
type ConfigurableEvilService struct {
CallRealFunction bool
}
func (s *ConfigurableEvilService) EvilFunction() error {
if s.CallRealFunction {
return external.EvilFunction()
}
return nil
}
// 测试中
func TestConfigurableEvilService(t *testing.T) {
// 测试模拟模式
service := &ConfigurableEvilService{CallRealFunction: false}
err := service.EvilFunction()
// 断言模拟行为
// 生产代码使用true
prodService := &ConfigurableEvilService{CallRealFunction: true}
_ = prodService
}
5. 覆盖率报告处理
在生成覆盖率报告时,可以通过以下方式排除特定文件:
# 排除生产实现文件
go test -coverprofile=coverage.out ./... -coverpkg=./...
go tool cover -html=coverage.out -o coverage.html
或者在代码中指定排除:
// real_evil_service.go 文件开头添加
// Code generated by go generate; DO NOT EDIT.
// This file is excluded from test coverage.
6. 实际项目中的实践
// 使用wire进行依赖注入
var ProviderSet = wire.NewSet(
wire.Struct(new(RealEvilService), "*"),
wire.Bind(new(EvilService), new(*RealEvilService)),
wire.Struct(new(MyService), "*"),
)
// 测试中使用
func TestSuite(t *testing.T) {
suite.Run(t, new(MyServiceTestSuite))
}
type MyServiceTestSuite struct {
suite.Suite
mockEvilService *MockEvilService
service *MyService
}
func (s *MyServiceTestSuite) SetupTest() {
s.mockEvilService = &MockEvilService{}
s.service = NewMyService(s.mockEvilService)
}
func (s *MyServiceTestSuite) TestProcess() {
err := s.service.Process()
s.NoError(err)
}
关键点总结:
- 接口隔离:通过接口将外部依赖抽象化
- 依赖注入:通过构造函数或方法注入依赖
- 测试覆盖:业务逻辑可达100%,生产适配器通过构建标签或配置控制
- 务实态度:100%覆盖率是理想目标,但某些外部依赖包装器可合理排除
这种方法既保持了代码的可测试性,又对难以测试的外部依赖进行了合理处理。覆盖率工具可以配置为排除这些特定的包装器实现,专注于核心业务逻辑的覆盖。

