Golang Go语言中如何使用依赖注入的方式实现TDD并避免注入冗余参数

发布于 1周前 作者 wuwangju 来自 Go语言

Golang Go语言中如何使用依赖注入的方式实现TDD并避免注入冗余参数

最近想学习一下 TDD,在网上找到了一篇文章 learn-go-with-tests
中间使用了依赖注入方式来预留给测试进行 MOCK,这部分我怎么看怎么感觉别扭,如果我需要根据 if else 初始化不同的资源呢?

func Exec(v1 int, v2 string) int {
	count := 0
	if v1 > 10 {
		q := NewQuery()
		q.Query(v1)
		count += 1
	}
	if strings.HasPrefix(v2, "a") {
		d := NewUpdate()
		d.Update(v2)
		count += 2
	}
	return count
}

如果要使用依赖注入,是要改成下面的样子么?

func ExecDI(v1 int, v2 string, q QueryModule, d UpdateModule) int {
	count := 0
	if v1 > 10 {
		q.Query(v1)
		count += 1
	}
	if strings.HasPrefix(v2, "a") {
		d.Update(v2)
		count += 2
	}
	return count
}

那如果子模块也有资源需要初始化,那也要从最顶层传进去么?

func ExecDIDI(v1 int, v2 string, q QueryModule, d UpdateModule, configs config.Config, db *sql.DB) int {
	count := 0
	if v1 > 10 {
		q.QueryDI(v1, configs)
		count += 1
	}
	if strings.HasPrefix(v2, "a") {
		d.UpdateDI(v2, db)
		count += 2
	}
	return count
}

假设一个偏远的代码分支需要从 Http 获取一些数据,本来只要执行到分支这里的时候,再让它自行去获取就好。
那改成依赖注入会变成从一开始就要获取么?
如果还是保留这部分不注入,那么这部分代码块就没法 mock 以进行充分的测试了


更多关于Golang Go语言中如何使用依赖注入的方式实现TDD并避免注入冗余参数的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

10 回复

<br>type Executor struct {<br> q QueryModel<br> d UpdateModel<br>}<br><br>func (e Executor) Exec(v1 int, v2 string) int{<br> count := 0<br> if v1 &gt; 10 {<br> e.q.Query(v1)<br> count += 1<br> }<br> if strings.HasPrefix(v2, "a") {<br> e.d.Update(v2)<br> count += 2<br> }<br> return count<br>}<br>

更多关于Golang Go语言中如何使用依赖注入的方式实现TDD并避免注入冗余参数的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html



这还是不能解决问题啊,QueryModel 和 UpdateModel 在哪里初始化?初始化时候所需的资源从哪里来?
我上面的示例只是一个简化的模式,实际上 QueryModel 和 UpdateModel 也可能包含的还有子模块,子模块也需要初始化

  1. 对于这个单测来说,他依赖的只是 QueryModel 的 Query 方法;如果 QueryModel 是个接口的话,你随便注入一个实现他的就可以
    2. 你这个测试如果还 QueryModel 的子模块就不对了;
    3. QueryModel 如何初始化那是另外的事情,一般是在 Executor 实例化时初始化注入( Golang 中推荐的实际是一个 struct 使用一个工厂方法来实例化);单测时候直接注入 mock 的

上述 2, “你这个测试如果还 QueryModel 的子模块就不对了” -> “你这个测试如果还依赖 QueryModel 的子模块就不对了”

你需要一个 Service 层或者 Repository 层, 然后把他们注入进来

参数注入本来就会碰到楼主说的这种问题。

所以我们都用依赖注入容器,让容器来管理。

楼主这种“如果子模块也有资源需要初始化,那也要从最顶层传进去么?”问题,
只需要在容器里搞个原资源类似别名的新资源代替原资源就行。


推荐个刚刚看到的 golang 依赖注入容器
https://github.com/bassbeaver/gioc


这只是一个简化模型…我知道你的方式可以完成测试,但是如果面对更复杂的状况呢?
假设在第一个分支里,query 返回 a,这个 a 的返回是依赖于 query 子模块进行的一些 http 操作而来的,a 会被用作 exec 函数下面的分支里进行判断,那么如果不能对 query 的子模块进行 mock,就无法控制 a 的返回,那么 exec 的测试就无法全覆盖。

我想表达的是冲突就在于如果需要注入依赖,那么是不是就得全注进去?如果不是全部注入,就意味着子模块有独立的数据源,不受上层模块的控制。如果是全部注入,那么问题就是提前无法确认哪些资源会被用到。

5,6 楼的方法应该可行…但是就感觉本来简单的结构变得太复杂了

“假设在第一个分支里,query 返回 a,这个 a 的返回是依赖于 query 子模块进行的一些 http 操作而来的,a 会被用作 exec 函数下面的分支里进行判断,那么如果不能对 query 的子模块进行 mock,就无法控制 a 的返回,那么 exec 的测试就无法全覆盖”

这时候你应该在多个测试用例中直接 mock query 返回不同的 a ;


你说的对…我把自己绕进去了

在Golang中,实现依赖注入(DI)以促进测试驱动开发(TDD)并避免冗余参数,通常可以通过接口和结构体组合来实现。以下是一些关键步骤和策略:

  1. 定义接口:首先,为需要注入的依赖定义接口。接口定义了依赖的行为,使得测试时可以用模拟(mock)对象替换实际实现。

  2. 使用结构体和方法接收者:在结构体中,通过接口类型的字段来持有依赖。这样,可以在实例化结构体时注入具体的依赖实现。

  3. 构造函数注入:通过构造函数将依赖注入到结构体中。这避免了在方法调用时传递大量参数,同时也使代码更清晰。

  4. 利用依赖注入框架(可选):虽然Go没有内置的DI框架,但可以使用像WireUber's dig这样的第三方库来自动化依赖注入过程,减少手动编码工作量。

  5. 编写单元测试:使用Go的测试框架(testing包)编写单元测试。在测试中,可以使用接口的模拟对象来替换实际依赖,从而专注于测试业务逻辑。

  6. 避免冗余参数:如果多个方法共享相同的依赖,确保这些依赖在结构体的层级上被注入,而不是在每个方法中都作为参数传递。这减少了方法的签名长度,提高了代码的可读性。

通过上述方法,可以在Go中实现有效的依赖注入,促进TDD实践,同时保持代码的整洁和可维护性。

回到顶部