【问题标题】:How to avoid import cycles in mock generation?如何避免模拟生成中的导入周期?
【发布时间】:2018-06-22 10:41:38
【问题描述】:

简单的例子。

我有包裹 xxx。这个包包含:

  • 结构A
  • 接口 B 是 A 的一个字段
  • struct C 是 B 方法中的参数

    type A struct {
        SomeField B
    }
    
    type B interface {
        SomeMethod(c C)
    }
    

现在假设我想为结构 A 和模拟依赖项 B 创建单元测试。为了创建模拟,我正在使用模拟生成器。所有模拟都存储在通用的“模拟”文件夹中。

问题是生成的 mock 依赖于 xxx 包。发生这种情况是因为接口 B 的 SomeMethod 具有参数 xxx.C。

每当我尝试在 a_test.go 中导入我的模拟结构时,它都会因为循环导入问题而失败。 xxx 包在 a_test.go 中导入 mocks 包。和 mocks 包在我生成的 mock 中导入 xxx 包。

我需要一个平静的建议,对此最好的解决方法是什么?也许我的方法不够惯用。您将模拟存储在哪里?

【问题讨论】:

  • xxx package importing mocks package 为什么?
  • 因为 mocks 包中包含了针对 B 接口的 mock。我需要它来模拟 a_test.go 中结构 A 的依赖关系
  • 你说的是mock package importing xxx,我很困惑
  • 是的,因为 B 的模拟包含方法 SomeMethod ,该方法具有来自包 xxx (xxx.C) 的参数。它应该被导入。

标签: go


【解决方案1】:

您需要将测试放在不同的包下。

a.go在包xxx

a_test.go在包xxx_test

a_mock.go在包xxx_mock

这样a_test.go会依赖xxxxxx_mock,不会造成依赖循环。

另外,a.goa_test.go 可以在同一个文件夹下,如下所示:

xxx/
  - a.go
  - a_test.go
mock/
  - a_mock.go

【讨论】:

  • 如果 a_test 使用模拟(否则有什么意义),这意味着我将在 xxx 中导入 mock 并且在模拟中我正在导入 xxx - 所以这里我们有循环。
  • @codemuncher 怎么样? xxx_test 只依赖于xxxxxx_mock,而xxx_mock 只依赖于xxx。自己画一张图。循环在哪里?目录之间的循环依赖并不意味着包之间的依赖。
  • 我的评论仅针对第二个插图,您说 a.goa_test.go 可以在同一个文件夹下(是的,我将文件夹作为一个包读取,因为 Go 编译器也将它们作为一个包读取. 如果我试图将a_test.go 放在不同的包但在同一个目录中,那么它会在同一个目录中抛出多个包错误)。
  • PS:我喜欢你把a_test.go放在不同的包下的想法
【解决方案2】:

自从

  1. 界面
  2. 结构
  3. 用户代码
  4. 用户代码的单元测试

都在同一个包中,接口应该被认为是“包内”接口,对于其他包中的代码是不可见的。所以,这个接口的模拟应该和接口本身在同一个包中。

结论:

  1. 界面
  2. 结构
  3. 用户代码
  4. 用户代码的单元测试
  5. 模拟界面

将它们全部放在同一个包中。

所以把gomock生成的mock代码和接口放到同一个包里,而不是“mock”包里。示例(windows 版本):

mockgen -source=.\foo\bar.go -destination=.\foo\bar_mock.go -package=foo

【讨论】:

    【解决方案3】:

    使用所有其他包从中导入的顶级包。把你的接口放在那里。

    例如:

    domain/
        interfaces.go
    a/
        mock.go
    b/
        mock.go
    c/
        mock.go
    

    abc 应该从 domain 导入,因此它们之间没有任何依赖关系。您将使用鸭子类型在您的模拟中实现 domain 包的接口。

    这是一个使用您的示例的实际用例:

    domain/interfaces.go

    type A interface {
        Foo()
    }
    
    type B interface {
        Bar() string
    }
    
    type C interface {
        Baz() string
    }
    

    a/mock.go

    type A struct {
        SomeField domain.B
    }
    
    // ...
    

    b/mock.go

    type B struct {
        SomeMethod(c domain.C)
    }
    
    // ...
    

    c/mock.go

    type C struct {}
    
    // ...
    

    这应该编译得很好,因为所有的 mock 都是从顶级 domain 包导入的,并且它们都实现了各自的接口。

    【讨论】:

    • 您的意思是不是将所有模拟集中在公共“模拟”文件夹中,而是每个包都有 mock.go,这个特定包的所有模拟都将存在?
    • 不,您也可以使用一个模拟文件夹。这里的重点是有一个顶级的domain 包,它具有“真正的”接口,而你的模拟只是这些顶级接口的鸭式实现。这为您提供了不会出现任何循环依赖错误的灵活性,因为模拟将全部从 domain 包导入并且不会相互依赖。
    • 那么在这种情况下,我会将所有应用程序接口存储在 domain/interfaces.go 中,而不是逻辑上应该属于的包中?这种方法是惯用的/经常使用的吗?
    • 实际上,将您的方法应用于我的示例,如果我将接口 B 移动到域包,域将取决于 xxx,因为接口方法具有 xxx.C 参数。并且 xxx 应该依赖于域,因为 B 是 A 的一部分。因此,如果将接口 B 移动到域,则 C 也应该移动。
    • 这是一个Clean Architecture 设计模式。如果它适合您的需要,请使用它,否则请丢弃它。这只是您的循环依赖问题的一种解决方案。作为对您最后评论的回应,是的,这是正确的。我的示例也假设了这一点。
    【解决方案4】:

    如果在 Go 1.17 中避免自引用

    添加这个参数

    -self_package github.com/xxx
    

    所以

    mockgen -source=<srcName>.go \
    -package <pkgName> \
    -self_package github.com/<user>/<repo>/<pkgName> \
    -destination <dest>.go
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-01-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-23
      • 1970-01-01
      • 2015-10-29
      相关资源
      最近更新 更多