Search code examples
godependency-injectiongo-interface

Mock interface return type for dep injection


EDIT

As pointed out in the accepted answer, the issue here was doing go duck typing in the wrong direction. I'd like to add the following github issue as an attachment, since it provided me useful information in addition to @matt answer below:

https://github.com/golang/mock/issues/316#issuecomment-512043925

ORIGINAL POST

I'm new to dependencies injection, and wanted to test it on a module that uses couchbase go sdk. For this purpose I need interfaces to reproduce both Cluster and Bucket structures.

On the Cluster interface I need the Bucket() method, which has the following signature:

func (c *gocb.Cluster) Bucket(bucketName string) *gocb.Bucket

I also need the two following methods from the Bucket interface:

func (b *gocb.Bucket) Collection(collectionName string) gocb.*Collection
func (b *gocb.Bucket) DefaultCollection() *gocb.Collection

The tricky part is that both Cluster and Bucket methods have pointer receivers. This isn't hard in itself, since I know how to mock such methods alone (you just need to use a pointer to the type that implements the interface).

The issue is that one of the Cluster methods needs to return a pointer that implements the Bucket interface, since it also has pointer receivers methods. I tried many combinations, but each time I use an non-mocked *gocb.Cluster value as an argument to one of my functions, it fails because the Bucket method on the cluster instance isn't implemented correctly by the instance.

Below is my latest attempt:

package deps

import (
    "github.com/couchbase/gocb/v2"
)

// Database mocks the gocb.Cluster interface.
type Database interface {
    Bucket(bucketName string) *Bucket
}

// Bucket mocks the gocb.Bucket interface.
type Bucket interface {
    Collection(collectionName string) *gocb.Collection
    DefaultCollection() *gocb.Collection
}

The linter then returns the following error whenever I try to use an actual gocb.Cluster value:

linter error 1

I also tried to replace the Bucket method signature in my Database interface with:

// Database mocks the gocb.Cluster interface.
type Database interface {
    Bucket(bucketName string) Bucket
}

Which again gives me the following lint error:

linter error 2

How can I implement an interface to mock both methods ?


Solution

  • I think the key concept that you're missing is that the mock object has to match the interface requirements of what you're mocking. That includes the parameters and return values of the methods.

    type Database interface {
        // Bucket(bucketName string) *Bucket   // Wrong
        Bucket(bucketName string) *gocb.Bucket // Correct
    }
    

    You can still use the return value of Database.Bucket as a deps.Bucket, given that you've also mocked that interface properly.

    Unless I'm missing something about your testing process, this should do what you need.

    package main
    
    import (
        "github.com/couchbase/gocb/v2"
    )
    
    // Database mocks the gocb.Cluster interface.
    type Database interface {
        Bucket(bucketName string) *gocb.Bucket
    }
    
    // Bucket mocks the gocb.Bucket interface.
    type Bucket interface {
        Collection(collectionName string) *gocb.Collection
        DefaultCollection() *gocb.Collection
    }
    
    func someFunc(db Database) *gocb.Bucket {
        return db.Bucket("")
    }
    
    func anotherFunc(bucket Bucket) {
        bucket.Collection("")
    }
    
    func main() {
        var cluster *gocb.Cluster
        bucket := someFunc(cluster)
        anotherFunc(bucket)
    }