我想编写一个单元测试,在其中我运行一个临时 gRPC 服务器,该服务器在测试中的一个单独的 Goroutine 中启动,并在测试运行后停止。为此,我尝试将本教程 (https://grpc.io/docs/languages/go/quickstart/) 中的“Hello, world”示例改编为其中的服务器和客户端,而不是具有单独的 main.go
s,是一个单独的测试函数,它异步启动服务器,随后使用 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
我不知道为什么没有建立连接?在我看来,服务器已正确启动...
最佳答案
您在此处发布的代码似乎有错字:
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()
Without this, Dial returns immediately and connecting the server happens in background.
使用这个选项,任何连接错误都应该直接从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.WithTransportCredentials(insecure.NewCredentials()),
grpc.FailOnNonTempDialError(true),
grpc.WithBlock(),
)
如果连接字符串错误,则会立即失败并显示相应的错误消息。
关于go - 使用 WithBlock() 选项创建 gRPC 客户端连接到异步启动的 gRPC 服务器会无限期地阻塞?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62663990/