【Golang】如何测试TCP/UDP连接-文章读后感

说明

随笔记录一下阅读博客后的启发和感想。
博客原文:
How to test TCP/UDP connections in Go - Part 1
How to test TCP/UDP connections in Go - Part 2

启发

文章是作者William Gough是一个全栈的JS开发者,同时也是一个Gopher。他的这两篇文章向我们清晰明了的展示了创建tcp/udp服务和client连接的过程。

作者没有在文章中先实现server的代码,再编写测试用例来对代码进行测试。而是通过两个测试方法TestNETServer_RunTestNETServer_Request 向我们传达了测试驱动的开发方法。

分析

作者首先通过如下代码向我们展示需要创建一个监听于特定端口的socket server:

package net

import (
    "log"
)

var srv Server

func init() {
    // Start the new server.
    srv, err := NewServer("tcp", ":1123")
    if err != nil {
        log.Println("error starting TCP server")
        return
    }

    // Run the server in Goroutine to stop tests from blocking
    // test execution.
    go func() {
        srv.Run()
    }()
}

其中NewServer方法还没有定义和实现,在这里只是个口子。再通过编写一个TestNETServer_Run测试方法,我们就能明确的知道服务端需要提供一个支持tcp协议的监听1123端口的socket服务:

// Below init function
func TestNETServer_Run(t *testing.T) {
   // Simply check that the server is up and can
   // accept connections.
   conn, err := net.Dial("tcp", ":1123")
   if err != nil {
       t.Error("could not connect to server: ", err)
   }
   defer conn.Close()
}

在实现NewServer方法时作者明确的知道将来还需要一个支持udp协议的socket服务,而tcp和udp服务的行为模式是相同的只是具体的实现方式有所不同。这时候就可以抽象出一个interface表示这一类的server【作者在文章中提醒我们,在做这类抽象设计时要考虑这样做的必要性以防止过度设计】:

// Server defines the minimum contract our
// TCP and UDP server implementations must satisfy.
type Server interface {
    Run() error
    Close() error
}

// NewServer creates a new Server using given protocol
// and addr.
func NewServer(protocol, addr string) (Server, error) {
    switch strings.ToLower(protocol) {
    case "tcp":
        return &TCPServer{
            addr: addr,
        }, nil
    case "udp":
    	 return &UDPServer{
            addr: addr,
        }, nil
    }
    return nil, errors.New("Invalid protocol given")
}

然后再由TCPServer和UDPServer分别实现Run和Close方法。 在实现TCPServer的过程中作者并没有急着去实现具体的业务功能:

// implementation.
type TCPServer struct {
    addr   string
    server net.Listener
}

// Run starts the TCP Server.
func (t *TCPServer) Run() (err error) {
    t.server, err = net.Listen("tcp", t.addr)
    if err != nil {
        return
    }
    for {
        conn, err := t.server.Accept()
        if err != nil {
            err = errors.New("could not accept connection")
            break
        }
        if conn == nil {
            err = errors.New("could not create connection")
            break
        }
        conn.Close()
    }
    return
}

// Close shuts down the TCP Server
func (t *TCPServer) Close() (err error) {
    return t.server.Close()
}

而是通过另外一个测试方法TestNETServer_Request来驱动完善具体的业务逻辑。

func TestNETServer_Request(t *testing.T) {
    tt := []struct {
        test    string
        payload []byte
        want    []byte
    }{
        {
            "Sending a simple request returns result",
            []byte("hello world\n"),
            []byte("Request received: hello world")
        },
        {
            "Sending another simple request works",
            []byte("goodbye world\n"),
            []byte("Request received: goodbye world")
        },
    }

    for _, tc := range tt {
        t.Run(tc.test, func(t *testing.T) {
            conn, err := net.Dial("tcp", ":1123")
            if err != nil {
                t.Error("could not connect to TCP server: ", err)
            }
            defer conn.Close()

            if _, err := conn.Write(tc.payload); err != nil {
                t.Error("could not write payload to TCP server:", err)
            }

            out := make([]byte, 1024)
            if _, err := conn.Read(out); err == nil {
                if bytes.Compare(out, tc.want) == 0 {
                    t.Error("response did match expected output")
                }
            } else {
                t.Error("could not read from connection")
            }
        })
    }
}

通过测试方法我们可以看到,客户端通过socket传给服务端一串字符串,服务端要在收到的字符串前添加Request received:并返回给客户端。明确要求后编写服务端代码来通过测试:

func (t *TCPServer) handleConnections() (err error) {
    for {
        conn, err := t.server.Accept()
        if err != nil || conn == nil {
            err = errors.New("could not accept connection")
            break
        }

        go t.handleConnection(conn)
    }
    return
}

func (t *TCPServer) handleConnection(conn net.Conn) {
    defer conn.Close()

    rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
    for {
        req, err := rw.ReadString('\n')
        if err != nil {
            rw.WriteString("failed to read input")
            rw.Flush()
            return
        }

        rw.WriteString(fmt.Sprintf("Request received: %s", req))
        rw.Flush()
    }
}

最后输出测试结果:

=== RUN   TestNETServer_Running
-------- PASS: TestNETServer_Running (0.00s)
=== RUN   TestNETServer_Request
=== RUN   TestNETServer_Request/Sending_a_simple_request_returns_result
=== RUN   TestNETServer_Request/Sending_another_simple_request_works
-------- PASS: TestNETServer_Request (0.00s)
    --- PASS: TestNETServer_Request/Sending_a_simple_request_returns_result (0.00s)
    --- PASS: TestNETServer_Request/Sending_another_simple_request_works (0.00s)
PASS
coverage: 68.6% of statements

总结

我们可以看到作者的代码中,没有一次性编写完所有的业务实现。而是通过测试用例来明确需求,由于需要通过测试,我们“被迫”不断的完善业务代码。这种方式一下让我想到了曾经看过的Robert C. Martin的书中提到的一些设计开发原则。
推荐书籍:《代码整洁之道》、《敏捷软件开发:原则、模式与实践》、《架构整洁之道》

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值