我正在try 为Go AMQP使用者https://github.com/rabbitmq/amqp091-go/blob/main/_examples/consumer/consumer.go的精简和略微修改的版本编写单元测试,以使用一个简单的实用程序来消费RabbitMQ队列中的消息,并将它们转发到AWS SQS队列.我在模仿Connection和Channel struct 之类的东西时遇到了困难--这是一个很新的东西--有什么 idea 可以解决这个问题吗?我在操场上写了一个代码要点--go 掉了大部分代码,只剩下突出的部分.问题是这样的:

https://go.dev/play/p/ybSB6EU3siO

go: finding module for package github.com/rabbitmq/amqp091-go
go: downloading github.com/rabbitmq/amqp091-go v1.8.1
go: found github.com/rabbitmq/amqp091-go in github.com/rabbitmq/amqp091-go v1.8.1
# play
./prog.go:23:16: cannot use dial(url) (value of type *amqp091.Connection) as Connection value in assignment: *amqp091.Connection does not implement Connection (wrong type for method Channel)
        have Channel() (*amqp091.Channel, error)
        want Channel() (Channel, error)

Go build failed.

我试图嘲弄的真正乐趣是: https://github.com/rabbitmq/amqp091-go/blob/579207b03cecc66c1b206679b79f267c8c734db7/connection.go#L843

用于说明该问题的示例代码.

package main

import amqp "github.com/rabbitmq/amqp091-go"

type Channel interface {
    Cancel(consumer string, noWait bool) error
}

type Connection interface {
    Channel() (Channel, error)
    Close() error
}

type Consumer struct {
    conn    Connection
    channel Channel
    tag     string
    done    chan error
}

func (c *Consumer) Consume(url string) error {
    var err error
    c.conn, err = dial(url)
    return err
}

var dial = func(url string) (*amqp.Connection, error) {
    return &amqp.Connection{}, nil
}

func main() {

}

// mocks in a test file

type mockConnection struct{}

func (m *mockConnection) Channel() (Channel, error) {
    return &mockChannel{}, nil
}

func (m *mockConnection) Close() error {
    return nil
}

type mockChannel struct{}

func (m *mockChannel) Cancel(consumer string, noWait bool) error {
    return nil
}

//dial = func(url string) (*mockConnection, error) {
//  return &mockConnection{}, nil
//}

因此,在模拟代码中,我可以在此处使用Channel而不是*moockChannel:

func (m *mockConnection) Channel() (Channel, error) {
    return &mockChannel{}, nil
}

但很明显,我不能更改amqp091代码来这样做,因此我被难住了.

推荐答案

我认为在rabbitmq/amqp091-go connection.go年内:

/*
Channel opens a unique, concurrent server channel to process the bulk of AMQP
messages.  Any error from methods on this receiver will render the receiver
invalid and a new Channel should be opened.
*/
func (c *Connection) Channel() (*Channel, error) {
    return c.openChannel()
}

但你有:

type Connection interface {
    Channel() (Channel, error)
    Close() error
}

I would prefer using MyConnection or MyChannel, just to avoid any confusion.
And using a dial function returning (MyConnection, error) instead of (*amqp.Connection, error): That would allow you to use either the real amqp connection or your mock connection in tests by assigning the dial function to return your mock implementation.

You can then use type assertions within your actual code to handle cases where you need to work with the real amqp091 structs.
For example:

func doSomethingWithChannel(channel MyChannel) {
    if realChannel, ok := channel.(*amqp.Channel); ok {
        // Do something with realChannel
    } else {
        // Handle mockChannel
    }
}

这允许您通过模拟ConnectionChannel接口来编写单元测试,同时仍然能够在实际代码中使用真正的amqp091 struct .

然而..这意味着您的测试代码在您的应用程序代码中泄漏,这不是最佳实践.

One way to avoid this is to have your production and test code both implement the same interface, and then in your actual code, you only interact with this interface.
This way, the actual code does not know anything about whether it is dealing with the real implementation or the mock, and you do not have any test code leaking into your actual application.

package main

import (
    "fmt"
    amqp "github.com/rabbitmq/amqp091-go"
)

// Define the interfaces
type MyChannel interface {
    Cancel(consumer string, noWait bool) error
}

type MyConnection interface {
    Channel() (MyChannel, error)
    Close() error
}

// Consumer struct
type Consumer struct {
    conn    MyConnection
    channel MyChannel
    tag     string
    done    chan error
}

func (c *Consumer) Consume(url string) error {
    var err error
    c.conn, err = dial(url)
    if err != nil {
        return err
    }
    c.channel, err = c.conn.Channel()
    return err
}

// Use function variable for dial so it can be overridden in tests
var dial = func(url string) (MyConnection, error) {
    conn, err := amqp.Dial(url)
    if err != nil {
        return nil, err
    }
    return &AMQPConnection{conn}, nil
}

// Wrappers around real amqp types to implement your interfaces
type AMQPConnection struct {
    *amqp.Connection
}

func (a *AMQPConnection) Channel() (MyChannel, error) {
    ch, err := a.Connection.Channel()
    if err != nil {
        return nil, err
    }
    return &AMQPChannel{ch}, nil
}

type AMQPChannel struct {
    *amqp.Channel
}

func (a *AMQPChannel) Cancel(consumer string, noWait bool) error {
    return a.Channel.Cancel(consumer, noWait)
}

func main() {
    c := &Consumer{}
    err := c.Consume("amqp://guest:guest@localhost:5672/")
    fmt.Println(err)
}

// In a separate _test.go file

type mockConnection struct{}

func (m *mockConnection) Channel() (MyChannel, error) {
    return &mockChannel{}, nil
}

func (m *mockConnection) Close() error {
    return nil
}

type mockChannel struct{}

func (m *mockChannel) Cancel(consumer string, noWait bool) error {
    return nil
}

在测试中,您可以覆盖dial函数以返回模拟实现:

func TestConsumer(t *testing.T) {
    dial = func(url string) (MyConnection, error) {
        return &mockConnection{}, nil
    }
    // rest of your test code
}

That way, your production code does not have any knowledge of the test code, and it interacts only through the interface, which is implemented by both the real and mock versions.
The test code can inject the mock implementation by overriding the dial function.


要模拟the closeChannel method进行测试,实际上不需要模拟该特定方法,而是需要模拟通常会调用它的 struct 和方法.

closeChannelConnection struct 的未导出方法,这意味着不能从包外部直接访问它.此方法是amqp091-go库的内部逻辑的一部分.

但是,您仍然可以通过模拟与该方法交互的公共方法和 struct 来测试依赖于该方法的行为.

在您的测试文件中:

type mockConnection struct {
    // ... you can keep state here if needed to simulate connection behavior ...
}

func (m *mockConnection) Channel() (MyChannel, error) {
    // Simulate behavior of opening a new channel
    return &mockChannel{}, nil
}

func (m *mockConnection) Close() error {
    // Simulate behavior of closing the connection, which would internally call closeChannel
    return nil
}

type mockChannel struct{}

func (m *mockChannel) Close() error {
    // Simulate behavior of closing the channel, which would internally call closeChannel
    return nil
}

func TestSomething(t *testing.T) {
    // Use the mockConnection in place of the real connection
    conn := &mockConnection{}
    
    // Your test logic here, e.g., opening and closing channels, and asserting the expected behavior
}

Your mock implementations simulate the behavior of the real Connection and Channel types.
Although this does not let you test the internal closeChannel method directly, it does allow you to test the behavior that relies on it by using the public interface.
This approach adheres to the best practices of unit testing, where you should test the public interface of a unit and not its internal implementation details.

Go相关问答推荐

正在使用terratest执行terraform脚本测试,但遇到错误退出状态1

切换选项卡时,Goland IDE中的光标自动转移

Go Regexp:匹配完整的单词或子字符串,或者根本不匹配

使用一元或服务器流将切片从GRPC服务器返回到客户端

戈姆:如何将一对一联系起来?

Date.Format正在输出非常奇怪的日期

我可以在Golang中的另一个类型参数的基础上约束一个类型的参数吗?

Golang:如何在不转义每个动作的情况下呈现模板的模板?

最长连续重复的字符golang

你如何在 Golang 代码中测试 filepath.Abs​​ 失败?

golang 中的可变参数函数

从数据库中带有 imageurl 的文件夹中获取图像,并在我的浏览器中用 golang 中的 echo 显示

使用go doc命令查看示例函数?

每次有人进入我的网站时如何运行特定功能?

使用 image/jpeg 编码导致图像饱和/错误像素

在 connect-go 拦截器中修改响应体

Go GCP 同时模拟两个服务帐户

Golang 泛型

Golang 查询扫描未将查询正确扫描到 struct 中

在 Go 泛型中,如何对联合约束中的类型使用通用方法?