创建使用指针接收器方法包装现有类型的接口

I need to test an app which uses Google Cloud Pubsub, and so must wrap its types pubsub.Client and pubsub.Subscriber for testing purposes. However, despite several attempts I can't get an interface around them which compiles.

The definitions of the methods I'm trying to wrap are:

func (s *Subscription) Receive(
    ctx context.Context, f func(context.Context, *Message)) error

func (c *Client) Subscription(id string) *Subscription

Here is the current code. The Receiver interface (wrapper around Subscriber) seems to work, but I suspect it may need to change in order to fix SubscriptionMaker, so I've include both.

Note: I've tried several variations of where to reference and dereference pointers, so please don't tell me to change that unless you have an explanation of why your suggested configuration is the correct one or you've personally verified it compiles.

import (
    "context"

    "cloud.google.com/go/pubsub"
)

type Receiver interface {
    Receive(context.Context, func(ctx context.Context, msg *pubsub.Message)) (err error)
}

// Pubsub subscriptions implement Receiver
var _ Receiver = &pubsub.Subscription{}

type SubscriptionMaker interface {
    Subscription(name string) (s Receiver)
}

// Pubsub clients implement SubscriptionMaker
var _ SubscriptionMaker = pubsub.Client{}

Current error message:

common_types.go:21:5: cannot use "cloud.google.com/go/pubsub".Client literal (type "cloud.google.com/go/pubsub".Client) as type SubscriptionMaker in assignment:
    "cloud.google.com/go/pubsub".Client does not implement SubscriptionMaker (wrong type for Subscription method)
        have Subscription(string) *"cloud.google.com/go/pubsub".Subscription
        want Subscription(string) Receiver

It can't be done.

When defining the type signature of a method on an interface, it must match exactly. func (c *Client) Subscription(id string) *Subscription returns a *Subscription, and a *Subscription is a valid Receiver, but it does not count as conforming to the interface method Subscription(string) Receiver. Go requires precise matching for function signatures, not the duck-typing style that it usually uses for interfaces.

First, for most uses, using the ptest package is probably a much easier approach for testing pubsub. But of course, your specific question can apply to any library, and the below approach can be useful for many things, not just mocking pubsub.


Your broader goal of using interfaces to mock a library like this, is doable. But it is complicated when the library you wish to mock out returns concrete types that you cannot mock (probably due to unreported fields). The approach to be taken is much more involved than is often worth it, as there may be easier ways to test your code.

But if you're intent on doing this, the approach you must take is to not wrap the entire package in interfaces, not just the specific methods you wish to mock.

You would need to wrap any types that you wish to mock which are returned by or accepted by your interface, too. This usually means you also need to modify your production code (not just your test code), so this can sometimes be a deal-breaker for existing code bases.

Where I have usually done this before is when mocking something like the standard library's sql driver, but the same approach can be applied here. In essence, you would need to create a wrapper package for your pubsub library, which you use even in your production code. Again, this can be quite intrusive on existing codebases, but for the sake of illustration. Using your defined interfaces:

package mypubsub

import "cloud.google.com/go/pubsub"

type Receiver interface {
    Recieve(context.Context, func(context.Context, *pubsub.Message) error)
}

type SubscriptionMaker interface {
    Subscription(string) Receiver
}

You can then wrap the default implementation, for use in production code:

// defaultClient wraps the default pubsub Client functionality.
type defaultClient struct {
    *pubsub.Client
}

func (d defaultImplementation) Subscription(name string) Receiver {
    return d.Client.Subscription()
}

Naturally, you'd need to expand this package to wrap most or all of the pubsub package you're using. This can be a bit daunting.

But once you've done that, then use your mypubsub package everywhere in your code, instead of directly depending on the pubsub package. And now you can easily swap out a mock implementation anywhere you need for testing.