我想编写一个单元测试,其中运行一个短暂的gRPC服务器,该服务器在测试中的单独Goroutine中启动,并在测试运行后停止。为此,我尝试将本教程(https://grpc.io/docs/languages/go/quickstart/)中的“ Hello,world”示例改编为一个示例,在该示例中,只有一个单独的main.go
而不是服务器和客户端测试功能,该功能异步启动服务器,随后使用grpc.WithBlock()
选项建立客户端连接。
我已将简化示例放在此存储库中,https://github.com/kurtpeek/grpc-helloworld;在这里是main_test.go
:
package main
import (
"context"
"fmt"
"log"
"net"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/examples/helloworld/helloworld"
)
const (
port = ":50051"
)
type server struct {
helloworld.UnimplementedGreeterServer
}
func (s *server) SayHello(ctx context.Context, in *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &helloworld.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func TestHelloWorld(t *testing.T) {
lis, err := net.Listen("tcp", port)
require.NoError(t, err)
s := grpc.NewServer()
helloworld.RegisterGreeterServer(s, &server{})
go s.Serve(lis)
defer s.Stop()
log.Println("Dialing gRPC server...")
conn, err := grpc.Dial(fmt.Sprintf("localhost:%s", port), grpc.WithInsecure(), grpc.WithBlock())
require.NoError(t, err)
defer conn.Close()
c := helloworld.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
log.Println("Making gRPC request...")
r, err := c.SayHello(ctx, &helloworld.HelloRequest{Name: "John Doe"})
require.NoError(t, err)
log.Printf("Greeting: %s", r.GetMessage())
}
问题是,当我运行此测试时,它超时:
> go test -timeout 10s ./... -v
=== RUN TestHelloWorld
2020/06/30 11:17:45 Dialing gRPC server...
panic: test timed out after 10s
我无法查看为什么未建立连接?在我看来,服务器已正确启动...
答案 0 :(得分:3)
您在此处发布的代码似乎有错字:
fmt.Sprintf("localhost:%s", port)
如果我在不使用grpc.WithBlock()
选项的情况下运行测试功能,则c.SayHello
会出现以下错误:
rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp: address localhost::50051: too many colons in address"
罪魁祸首似乎是localhost::50051
从const
声明中删除多余的冒号(或从fmt.Sprintf("localhost:%s", port)
中删除,如果需要的话),测试通过。
const (
port = "50051" // without the colon
)
输出:
2020/06/30 23:59:01 Dialing gRPC server...
2020/06/30 23:59:01 Making gRPC request...
2020/06/30 23:59:01 Received: John Doe
2020/06/30 23:59:01 Greeting: Hello John Doe
但是,根据grpc.WithBlock()
否则,Dial将立即返回,并且在后台进行服务器连接。
因此,使用此选项,应该从grpc.Dial
调用中立即返回所有连接错误:
conn, err := grpc.Dial("bad connection string", grpc.WithBlock()) // can't connect
if err != nil {
panic(err) // should panic, right?
}
那为什么您的代码会挂起?
通过查看grpc
包的源代码(我针对v1.30.0
构建了测试):
// A blocking dial blocks until the clientConn is ready.
if cc.dopts.block {
for {
s := cc.GetState()
if s == connectivity.Ready {
break
} else if cc.dopts.copts.FailOnNonTempDialError && s == connectivity.TransientFailure {
if err = cc.connectionError(); err != nil {
terr, ok := err.(interface {
Temporary() bool
})
if ok && !terr.Temporary() {
return nil, err
}
}
}
if !cc.WaitForStateChange(ctx, s) {
// ctx got timeout or canceled.
if err = cc.connectionError(); err != nil && cc.dopts.returnLastError {
return nil, err
}
return nil, ctx.Err()
}
}
因此,s
目前确实处于TransientFailure
状态,但是FailOnNonTempDialError
选项默认为false
,并且当上下文过期时WaitForStateChange
为false ,这不会发生,因为Dial
与背景上下文一起运行:
// Dial creates a client connection to the given target.
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
return DialContext(context.Background(), target, opts...)
}
目前,我不知道这是否是预期的行为,因为v1.30.0
中的某些API被标记为实验性的。
无论如何,最终要确保在Dial
上捕获到此类错误,您还可以将代码重写为:
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithInsecure(),
grpc.FailOnNonTempDialError(true),
grpc.WithBlock(),
)
如果连接字符串错误,则会立即失败并显示相应的错误消息。