说明
随笔记录一下阅读博客后的启发和感想。
博客原文:
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_Run
和TestNETServer_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的书中提到的一些设计开发原则。
推荐书籍:《代码整洁之道》、《敏捷软件开发:原则、模式与实践》、《架构整洁之道》