Featured image of post 使用bufconn实现GRPC单元测试

使用bufconn实现GRPC单元测试

如何在不启动本地端口监听的情况下,进行GRPC单元测试,降低环境要求和性能成本

初始化bufconn连接器

传统的单元测试,我们需要启动一个服务端,然后再启动一个客户端,首先需要占用我们的端口,其次资源开销还大。

因此我们可以使用google提供的bufconn包,该包可以在内存上创建一个虚拟接口用于连接,资源消耗低,且不占用端口

同时由于他是运行在内存上的,因此你只能老实手动在单元测试内初始化他,而不能说跨进程进行测试

// 全局定义一个buff区域,该区域暂存整个GRPC请求req和Res,合理设置即可
const bufSize = 1024 * 1024
// 全局定一个bufListener 这样可以提高拨号器的利用率  
var bufListener *bufconn.Listener
func init() {
	bufListener := bufconn.Listen(bufSize)
	s := grpc.NewServer()
	gs := NewGRPCServer(addService{})  // 新建一个GRPC服务,该服务所属结构体必须实现了所有方法,否则会报错
	pb.RegisterGRPCServerServer(s, gs) // 将GRPC服务注册
	go func() {                        // 开一个线程用来让Server后台运行,避免阻塞
		if err := s.Serve(bufListener); err != nil { // 将GRPC服务运行到bufconn上
			log.Fatalf("Server init Server Error from bufConn :%s", err.Error())
		}
	}()
}

完成后,我们就在内存上定义了一个端点,可以对外提供服务,这个Server的句柄就在bufListener

客户端实现连接

完成服务端的启动后,就应该启动客户端了

与传统的GRPC客户端连接不同,服务端定义bufconn后,他就不会对外监听接口了,此时我们创建连接就需要使用grpc.WithContextDialer(bufDialer) 进行实现了,而其中的入参bufDialer 就是我么说的连接对象了

默认情况下,连接对象是通过target实现的,也就是直接采用defaultDialOptions这个拨号器,如果你调用了WithContextDialer,则会使用你自定义的拨号器对象

默认拨号器源码

func defaultDialOptions() dialOptions {
	return dialOptions{
		copts: transport.ConnectOptions{
			ReadBufferSize:  defaultReadBufSize,
			WriteBufferSize: defaultWriteBufSize,
			UseProxy:        true,
			UserAgent:       grpcUA,
			BufferPool:      mem.DefaultBufferPool(),
		},
		bs:              internalbackoff.DefaultExponential,
		healthCheckFunc: internal.HealthCheckFunc,
		idleTimeout:     30 * time.Minute,
		defaultScheme:   "dns",
		maxCallAttempts: defaultMaxCallAttempts,
	}
}

我们这里需要替换为bufconn的Dial,因此实现一个自定义函数,该函数用于返回一个net.Conn 对象,然后交给WithContextDialer 转换为一个option ,并最终交给NewClient

// 拨号器,不再直接指定端口拨号,而是采用bufListener拨号(因此我们的target也就失去了意义)
func bufDialer(ctx context.Context, target string) (net.Conn, error) {
	return bufListener.Dial()
}

完成拨号器函数的编写后,就可以编写我们建立客户端连接部分了

func sum_test(){
	conn, err := grpc.NewClient(
		"passthrough:///bufnet", // 这里必须这么写
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithContextDialer(bufDialer)) // 这里实现了自定义拨号器的效果
	if err != nil {
		t.Errorf("Connect GRPC Server Error:%s", err.Error())
		t.Fail()
	}
	defer conn.Close()
	c := pb.NewGRPCServerClient(conn)
}

我们的Client初始化主要变化两点

  1. target变化为passthrough:///bufnet
  2. 添加了grpc.WithContextDialer(bufDialer):这里就是为了替换我们原有的Dial为bufconn,而替换方法就是调用我们拨号器创建函数,返回bufListener.Dial()这个net.Conn对象

到此,我们就完成了客户端和服务端的连接了

调用测试和断言

调用测试就不必多言了,尝试看下效果就行,看会不会返回错误,以及结果

这里需要说一个包github.com/stretchr/testify

这个包是Golang流行的单元测试包,他可以提供:断言操作 Mock 测试接口等操作

但是我们常用的就是断言操作了,他可以优化我们的代码长度

传统的代码测试

func TestSum(t *testing.T) {
	t.Logf("Sum开始测试\n")
	conn, err := grpc.NewClient(
		"passthrough:///bufnet",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithContextDialer(bufDialer))
	if err != nil {
		t.Errorf("Connect GRPC Server Error:%s", err.Error())
		t.Fail()
	}
	defer conn.Close()
	c := pb.NewGRPCServerClient(conn)
	// 测试相加方法
	res, err := c.Sum(context.TODO(), &pb.SumRequest{
		A: 3,
		B: 5,
	})
	// 单独的断言测试
	if err != nil {
		fmt.Printf("Sum Error:%s\n", err.Error())
		t.Fail()
	} // 测试err是否存在
	if res==nil{
		fmt.Printf("Res is Nil")
		t.Fail()
	} // 测试res对象是否不存在
	if res.V!=int64(8){
		fmt.Printf("Res.V != 8")
		t.Fail()
	} // 测试返回值是否相同
	t.Logf("Sum测试成功\n")
}

而调用了testify后,我们可以使用语句快速断言了,节约了大量的IF

func TestSum(t *testing.T) {
	t.Logf("Sum开始测试\n")
	conn, err := grpc.NewClient(
		"passthrough:///bufnet",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithContextDialer(bufDialer))
	if err != nil {
		t.Errorf("Connect GRPC Server Error:%s", err.Error())
		t.Fail()
	}
	defer conn.Close()
	c := pb.NewGRPCServerClient(conn)
	// 测试相加方法
	res, err := c.Sum(context.TODO(), &pb.SumRequest{
		A: 3,
		B: 5,
	})
	require.NoError(t, err)           // 测试err是否存在
	require.NotNil(t, res)            // 测试res对象是否不存在
	require.Equal(t, res.V, int64(8)) // 测试返回值是否相同,不相同则调用t对象下的error
	t.Logf("Sum测试成功\n")
}

具体使用方式就在上面了,自己看就行


如果你需要看错误信息,则一定要使用NotError这个方法去看,而不要使用Nil方法去看,Nil方法不会返回错误信息,这点请注意

使用NoError image-0ewhwrv1

使用Nil image-0ewhwrv1

时间流逝中|构建版本:75|构建时间:2025-01-28 23:18|Jenkins自动构建正常运行中
Built with Hugo
主题 StackJimmy 设计