为了账号安全,请及时绑定邮箱和手机立即绑定

如何优雅地测试 Go 中包含多个客户端的方法?

如何优雅地测试 Go 中包含多个客户端的方法?

Go
慕姐8265434 2022-06-13 10:19:39
我有一个Client包含多个客户端(etcd 和 libvirt)的结构。就像是:type Client struct {  etcd    *clientv3  libvirt *libvirt.Connect}一旦我的图书馆的客户想要关闭它的句柄,我想关闭这两个。所以我有:func (c *Client) Close() error {    c.etcd.Close()    c.libvirt.Close()    // Error handling excluded for brevity}什么是测试这个的优雅方法?我目前最好的选择是创建两个接口,一个用于两个包装客户端中的每一个。这些接口将包括我的库使用的两个客户端的每一个方法。这应该使得传递某种模拟而不是真正的客户变得相对容易。这可能是前进的方向,但感觉很尴尬。我还有哪些其他选择?
查看完整描述

2 回答

?
幕布斯6054654

TA贡献1876条经验 获得超7个赞

正如我在评论中提到的,您可以创建一个ClosableClient如下所示的。由于您的每个客户都有Close方法,因此您可以这样做。在您的测试文件中,您可以创建只需要实现Close方法的模拟客户端。您不需要使接口实现所有方法。在您的代码中,您可以使用类型断言将其转换ClosableClient为特定的客户端以访问其功能。这是类型断言的一个很好的例子。


我添加了代码片段来展示如何使用类型断言来获取底层结构。测试文件中的模拟客户端不需要实现 Foo 和 Bar 方法,因为接口ClosableClient只需要Close方法。


type ClosableClient interface {

    Close()

}


type Etcd struct{}


func (e *Etcd) Close() {

    fmt.Println("etcd closing")

}


func (e *Etcd) Foo() {

    fmt.Println("etcd foo")

}


type Libvirt struct{}


func (l *Libvirt) Close() {

    fmt.Println("libvirt closing")

}


func (l *Libvirt) Bar() {

    fmt.Println("libvirt bar")

}


type Client struct {

    etcd    ClosableClient

    libvirt ClosableClient

}


func (c *Client) Close() {

    c.etcd.Close()

    c.libvirt.Close()

}


func (c *Client) FooBar() {

    etcd, ok := c.etcd.(*Etcd)

    if !ok {

        panic("etcd is of incorrect type")

    }


    etcd.Foo()


    libvirt, ok := c.etcd.(*Libvirt)

    if !ok {

        panic("libvirt is of incorrect type")

    }


    libvirt.Bar()

}


查看完整回答
反对 回复 2022-06-13
?
有只小跳蛙

TA贡献1824条经验 获得超8个赞

受到 poWar 说我的想法很好的评论的鼓舞,我继续前进:


我更改了我的Client结构以将接口用于我的 libvirt 和 etcd 连接:


type EtcdClient interface {

}


type LibvirtClient interface {

}


type Client struct {

    etcd    EtcdClient

    libvirt LibvirtClient

}

当我尝试编译包时,我收到一条类似这样的错误消息:


./main.go:17:18: c.etcd.Close undefined (type EtcdClient is interface with no methods)

./main.go:21:24: c.libvirt.Close undefined (type LibvirtClient is interface with no methods)

不奇怪。然后我在接口中添加了最简单的 Close() 方法:


type EtcdClient interface {

    Close()

}


type LibvirtClient interface {

    Close()

}

再次编译给了我:


./main.go:56:10: cannot use etcd (type *clientv3.Client) as type EtcdClient in assignment:

    *clientv3.Client does not implement EtcdClient (wrong type for Close method)

        have Close() error

        want Close()

./main.go:62:13: cannot use lv (type *libvirt.Connect) as type LibvirtClient in assignment:

    *libvirt.Connect does not implement LibvirtClient (wrong type for Close method)

        have Close() (int, error)

        want Close()

然后我用它来填写接口定义:


type EtcdClient interface {

    Close() error

}


type LibvirtClient interface {

    Close() (int, error)

}

当然,Close这很简单,我不必经历这个,但正如我之前提到的,我在这些接口上调用了很多方法,这种方式让编译器帮助我填写接口变得非常简单定义。


对于测试,我可以制作假货(模拟?存根?我总是忘记区别)。这是完整的测试文件:


package main


import (

    "errors"

    "testing"

)


type FakeEtcdClient struct {

    wasClosed   bool

    failToClose bool

}


func (f *FakeEtcdClient) Close() error {

    if f.failToClose {

        return errors.New("Fake Etcd failed to Close")

    }

    f.wasClosed = true

    return nil

}


type FakeLibvirtClient struct {

    wasClosed   bool

    failToClose bool

}


func (f *FakeLibvirtClient) Close() (int, error) {

    if f.failToClose {

        return 0, errors.New("Fake libvirt failed to Close")

    }

    f.wasClosed = true

    return 0, nil

}


func TestClient_Close(t *testing.T) {

    type fields struct {

        etcd    EtcdClient

        libvirt LibvirtClient

    }

    tests := []struct {

        name    string

        fields  fields

        wantErr bool

    }{

        {"Happy path", fields{&FakeEtcdClient{}, &FakeLibvirtClient{}}, false},

        {"Etcd fails", fields{&FakeEtcdClient{failToClose: true}, &FakeLibvirtClient{}}, true},

        {"Libvirt fails", fields{&FakeEtcdClient{}, &FakeLibvirtClient{failToClose: true}}, true},

    }

    for _, tt := range tests {

        t.Run(tt.name, func(t *testing.T) {

            c := &Client{

                etcd:    tt.fields.etcd,

                libvirt: tt.fields.libvirt,

            }

            if err := c.Close(); (err != nil) != tt.wantErr {

                t.Errorf("Client.Close() error = %v, wantErr %v", err, tt.wantErr)

            } else {

                if !tt.wantErr {

                    // We only check if the clients have been closed if

                    // Client.Close() returns successfully.

                    if !c.etcd.(*FakeEtcdClient).wasClosed {

                        t.Error("Etcd connection was not closed")

                    }

                    if !c.libvirt.(*FakeLibvirtClient).wasClosed {

                        t.Error("Libvirt connection was not closed")

                    }

                }

            }

        })

    }

}



查看完整回答
反对 回复 2022-06-13
  • 2 回答
  • 0 关注
  • 103 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信