Search code examples
gomockingamqp

How to mock functions from libraries like amqp.Dial


I'm working on a small AMQP consumer and i want to test my consumer code, but im struggleing to mock the amqp.Dial. I have added some interface so i can mock Connection and Channel and added a property so i can control the dial-function:

//consumer.go
type AmqpChannel interface {
    ExchangeDeclare(name, kind string, durable, autoDelete, internal, noWait bool, args amqp.Table) error
    QueueDeclare(name string, durable, autoDelete, exclusive, noWait bool, args amqp.Table) (amqp.Queue, error)
    QueueBind(name, key, exchange string, noWait bool, args amqp.Table) error
    Consume(queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args amqp.Table) (<-chan amqp.Delivery, error)
    Publish(exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error
}

type AmqpConnection interface {
    Channel() (AmqpChannel, error)
    Close() error
}

type AmqpDial func(url string) (AmqpConnection, error)

type MyConsumer struct {
    HostDsn    string
    channel    AmqpChannel
    queue      amqp.Queue
    connection AmqpConnection
    DialFunc   AmqpDial
}

func (c *MyConsumer) Connect() error {
    var err error
    c.connection, err = c.DialFunc(c.HostDsn)
...

This seems to be close to what i want to achive, i can specify my test like this:

func TestConsumer(t *testing.T) {
    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish()

    var myConsumer = consumer.MyConsumer{
        HostDsn: "test",
        DialFunc: func(url string) (consumer.AmqpConnection, error) {
            return mocks.NewMockAmqpConnection(mockCtrl), nil
        },
    }
    _ = myConsumer.Connect()
}

But i cannot pass the original amqp.Dial as dial-func on the main routine:

myConsumer := consumer.MyConsumer{
        HostDsn: fmt.Sprintf(
            "amqp://%s:%s@rabbitmq:5672/?heartbeat=5s",
            os.Getenv("RABBITMQ_USER"),
            url.QueryEscape(os.Getenv("RABBITMQ_PASSWORD")),
        ),
        DialFunc: amqp.Dial,
    }

gives

./main.go:28:9: cannot use amqp.Dial (type func(string) (*amqp.Connection, error)) as type consumer.AmqpDial in field value

I hoped/thought that, as amqp.Connection fulfills the AmqpConnection interface, this would work :/ What is the proper way of mocking methods like amqp.Dial?

P.S.: im aware of https://github.com/NeowayLabs/wabbit but i would prefer to solve this on a lower level :)

Update: @mkopriva suggested to use another wrapper method, so i tried:

func getDialer(url string) (consumer.AmqpConnection, error) {
    var connection, err = amqp.Dial(url)
    return connection, err
}
myConsumer := consumer.myConsumer{
        HostDsn: fmt.Sprintf(
            "amqp://%s:%s@rabbitmq:5672/?heartbeat=5s",
            os.Getenv("RABBITMQ_USER"),
            url.QueryEscape(os.Getenv("RABBITMQ_PASSWORD")),
        ),
        DialFunc: getDialer,
    }

But this results in:

cannot use connection (type *amqp.Connection) as type consumer.AmqpConnection in return argument:
    *amqp.Connection does not implement consumer.AmqpConnection (wrong type for Channel method)
        have Channel() (*amqp.Channel, error)
        want Channel() (consumer.AmqpChannel, error)
make: *** [Makefile:4: build-consumer] Error 2

Solution

  • Given the following types:

    type AmqpChannel interface {
        ExchangeDeclare(name, kind string, durable, autoDelete, internal, noWait bool, args amqp.Table) error
        QueueDeclare(name string, durable, autoDelete, exclusive, noWait bool, args amqp.Table) (amqp.Queue, error)
        QueueBind(name, key, exchange string, noWait bool, args amqp.Table) error
        Consume(queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args amqp.Table) (<-chan amqp.Delivery, error)
        Publish(exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error
    }
    
    type AmqpConnection interface {
        Channel() (AmqpChannel, error)
        Close() error
    }
    
    type AmqpDial func(url string) (AmqpConnection, error)
    

    You can create simple wrappers that delegate to the actual code:

    func AmqpDialWrapper(url string) (AmqpConnection, error) {
        conn, err := amqp.Dial(url)
        if err != nil {
            return nil, err
        }
        return AmqpConnectionWrapper{conn}, nil
    }
    
    type AmqpConnectionWrapper struct {
        conn *amqp.Connection
    }
    
    // If *amqp.Channel does not satisfy the consumer.AmqpChannel interface
    // then you'll need another wrapper, a AmqpChannelWrapper, that implements
    // the consumer.AmqpChannel interface and delegates to *amqp.Channel.
    func (w AmqpConnectionWrapper) Channel() (AmqpChannel, error) {
        return w.conn.Channel()
    }
    
    func (w AmqpConnectionWrapper) Close() error {
        return w.conn.Close()
    }