How can I mock multiple types when the signature of a concrete method refers to another concrete type, not its interface?

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

How can I mock multiple types when the signature of a concrete method refers to another concrete type, not its interface?

问题

我正在使用一个没有为其类提供任何接口的第三方库。我可以在我的结构体中使用它们,没有问题,但它们会产生我在单元测试中想要避免的副作用。

// 在某个地方有一对结构体,没有接口。我不拥有这些代码。
// 每个结构体只有一个方法。
type ThirdPartyEntry struct {}
func (e ThirdPartyEntry) Resolve() string {
// 做一些带有副作用的复杂操作
return "我是我自己!"
}

// 这个结构体返回另一个结构体的实例。
type ThirdPartyFetcher struct {}
func (f ThirdPartyFetcher) FetchEntry() ThirdPartyEntry {
// 做一些带有副作用的复杂操作并返回一个实例
return ThirdPartyEntry{}
}

// 现在是我的代码。
type AwesomeThing interface {
BeAwesome() string
}
// 我有一个使用第三方库的类。
type Awesome struct {
F ThirdPartyFetcher
}
func (a Awesome) BeAwesome() string {
return strings.Repeat(a.F.FetchEntry().Resolve(), 3)
}
func NewAwesome(fetcher ThirdPartyFetcher) Awesome {
return Awesome{
F: fetcher,
}
}

func main() {
myAwesome := NewAwesome(ThirdPartyFetcher{})
log.Println(myAwesome.BeAwesome())
}

这个代码可以工作!但是我想编写一些单元测试,所以我想要模拟这两个第三方结构体。为此,我相信我需要为它们创建接口,但由于ThirdPartyFetcher返回ThirdPartyEntry,我无法弄清楚如何做到这一点。

我创建了一对与两个第三方类相匹配的接口。然后,我想重写Awesome结构体和方法,以使用通用的Fetcher接口。在我的测试中,我将调用NewAwesome(),传入一个testFetcher,这是一个也实现了该接口的结构体。

type Awesome struct {
F Fetcher
}
func NewAwesome(fetcher Fetcher) Awesome {
return Awesome{
Fetcher: fetcher,
}
}

type Entry interface {
Resolve() string
}
// 确认ThirdPartyEntry实现了Entry接口
var _ Entry = (*ThirdPartyEntry)(nil)

type Fetcher interface {
FetchEntry() Entry
}
// 确认ThirdPartyFetcher实现了Fetcher接口
var _ Fetcher = (*ThirdPartyFetcher)(nil)

我省略了测试代码,因为它与问题无关。但是这段代码在最后一行出错。

./main.go:49: cannot use (*ThirdPartyFetcher)(nil) (type *ThirdPartyFetcher) as type Fetcher in assignment:
*ThirdPartyFetcher does not implement Fetcher (wrong type for FetchEntry method)
have FetchEntry() ThirdPartyEntry
want FetchEntry() Entry

尽管我已经证明了ThirdPartyEntry实现了Entry接口,但签名不同。我相信这是不允许的,因为它会导致类似于切片的情况(在多态的意义上,而不是golang的意义)。有没有办法让我编写一对接口?Awesome类甚至不知道ThirdParty存在-它被接口抽象化,并在main调用NewAwesome时注入。

英文:

I'm making use of a third party library that doesn't have any interfaces for its classes. I can use them in my structs no problem, but they have side effects that I want to avoid when unit testing.

// Somewhere there are a couple structs, with no interfaces. I don't own the code.
// Each has only one method.
type ThirdPartyEntry struct {}
func (e ThirdPartyEntry) Resolve() string {
	// Do some complex stuff with side effects
	return "I'm me!"
}

// This struct returns an instance of the other one.
type ThirdPartyFetcher struct {}
func (f ThirdPartyFetcher) FetchEntry() ThirdPartyEntry {
	// Do some complex stuff with side effects and return an entry
	return ThirdPartyEntry{}
}

// Now my code.
type AwesomeThing interface {
    BeAwesome() string
}
// I have a class that makes use of the third party.
type Awesome struct {
	F ThirdPartyFetcher
}
func (a Awesome) BeAwesome() string {
	return strings.Repeat(a.F.FetchEntry().Resolve(), 3)
}
func NewAwesome(fetcher ThirdPartyFetcher) Awesome {
	return Awesome{
		F: fetcher,
	}
}

func main() {
	myAwesome := NewAwesome(ThirdPartyFetcher{})
	log.Println(myAwesome.BeAwesome())
}

This works! But I want to write some unit tests, and so I'd like to Mock both the third party structs. To do so I believe I need interfaces for them, but since ThirdPartyFetcher returns ThirdPartyEntrys, I cannot figure out how.

I created a pair of interfaces which match up with the two third party classes. I'd like to then rewrite the Awesome struct and method to use the generic Fetcher interface. In my test, I would call NewAwesome() passing in a testFetcher, a struct which also implements the interface.

type Awesome struct {
	F Fetcher
}
func NewAwesome(fetcher Fetcher) Awesome {
	return Awesome{
		Fetcher: fetcher,
	}
}

type Entry interface {
	Resolve() string
}
// Double check ThirdPartyEntry implements Entry
var _ Entry = (*ThirdPartyEntry)(nil)

type Fetcher interface {
	FetchEntry() Entry
}
// Double check ThirdPartyFetcher implements Fetcher
var _ Fetcher = (*ThirdPartyFetcher)(nil)

I omit the test code because it's not relevant. This fails on the last line shown.

./main.go:49: cannot use (*ThirdPartyFetcher)(nil) (type *ThirdPartyFetcher) as type Fetcher in assignment:
*ThirdPartyFetcher does not implement Fetcher (wrong type for FetchEntry method)
	have FetchEntry() ThirdPartyEntry
	want FetchEntry() Entry

The signatures are different, even though I already showed that ThirdPartyEntry implements Entry. I believe this is disallowed because to would lead to something like slicing (in the polymorphic sense, not the golang sense). Is there any way for me to write a pair of interfaces? It should be the case that the Awesome class doesn't even know ThirdParty exists - it's abstracted behind the interface and injected when main calls NewAwesome.

答案1

得分: 2

这不是很漂亮,但一种方法是:

type fetcherWrapper struct {
    ThirdPartyFetcher
}

func (fw fetcherWrapper) FetchEntry() Entry {
    return fw.ThirdPartyFetcher.FetchEntry()
}

我认为模拟返回结构体而不是接口的东西是一个相对常见的问题,除了大量的中间包装外,没有太好的解决方案。

英文:

It's not very pretty, but one way would be to:

type fetcherWrapper struct {
    ThirdPartyFetcher
}

func (fw fetcherWrapper) FetchEntry() Entry {
    return fw.ThirdPartyFetcher.FetchEntry()
}

I'd say mocking things that return structs vs interfaces is a relatively common problem without any great solutions apart from a lot of intermediate wrapping.

huangapple
  • 本文由 发表于 2015年11月13日 08:34:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/33684030.html
匿名

发表评论

匿名网友

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

确定