Bu örnekte basit bir gRPC istemcisi ve sunucu uygulaması oluşturacağız. Üçüncü parti taklit paketleri kullanmak yerine, test için yerel Golang bufconn paketini kullanacağız.


bufconn, bir sunucu başlatmanıza yardımcı olur, ancak gerçek bir soket/bağlantı noktasında değil. Test istemciniz, gerçek bir bağlantı noktası üzerinden gerçek bir sunucuyla konuşuyormuş gibi yapar. Bu paketle ilgili en iyi şey, testlerinizin gerçek bir ağ davranışı ile etkileşime girmesidir. Tüm bunlar klasik işletim sistemi düzeyindeki kaynakların aksine bellek içi bir bağlantı üzerinden gerçekleşir. Sonuç olarak normal bir ağ bağlantısının beklenen davranışını elde edersiniz.


Ön şartlar


gRPC kurulumu


go get -u google.golang.org/grpc

protobuf kurulumu


# MacOS
brew install protobuf

protoc-gen-go kurulumu


go get -u github.com/golang/protobuf/protoc-gen-go

bufconn kurulumu


go get -u google.golang.org/grpc/test/bufconn

Sunucu


Yapı


├── Makefile
├── cmd
│   └── server
│   └── main.go
├── internal
│   └── bank
│   └── account
│   ├── deposit_server.go
│   └── deposit_server_test.go
└── pkg
   └── proto
   └── bank
   └── account
   ├── deposit.pb.go
   └── deposit.proto

Dosyalar


Makefile

.PHONY: compile
compile: ## Compile the proto file.
protoc -I pkg/proto/bank/account/ pkg/proto/bank/account/deposit.proto --go_out=plugins=grpc:pkg/proto/bank/account/

.PHONY: run
run: ## Build and run server.
go build -race -ldflags "-s -w" -o bin/server cmd/server/main.go
bin/server

pkg/proto/bank/account/deposit.proto

syntax = "proto3";

package account;

option go_package = ".;account";

message DepositRequest {
float amount = 1;
}

message DepositResponse {
bool ok = 1;
}

service DepositService {
rpc Deposit(DepositRequest) returns (DepositResponse) {}
}

pkg/proto/bank/account/deposit.pb.go

Bu dosya make compile komutu ile yaratıldığından içeriğini göstermiyorum.


cmd/server/main.go

package main

import (
"log"
"net"

"github.com/inanzzz/client/internal/bank/account"
"google.golang.org/grpc"

pb "github.com/inanzzz/client/pkg/proto/bank/account"
)

func main() {
log.Println("Server running ...")

listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalln(err)
}

server := grpc.NewServer()

pb.RegisterDepositServiceServer(server, &account.DepositServer{})

log.Fatalln(server.Serve(listener))
}

internal/bank/account/deposit_server.go

package account

import (
"context"
"log"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

pb "github.com/inanzzz/client/pkg/proto/bank/account"
)

type DepositServer struct {
pb.UnimplementedDepositServiceServer
}

func (*DepositServer) Deposit(ctx context.Context, req *pb.DepositRequest) (*pb.DepositResponse, error) {
log.Println(req.GetAmount())

if req.GetAmount() < 0 {
return nil, status.Errorf(codes.InvalidArgument, "cannot deposit %v", req.GetAmount())
}

return &pb.DepositResponse{Ok: true}, nil
}

internal/bank/account/deposit_server_test.go

package account

import (
"context"
"fmt"
"log"
"net"
"testing"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"

pb "github.com/inanzzz/client/pkg/proto/bank/account"
)

func dialer() func(context.Context, string) (net.Conn, error) {
listener := bufconn.Listen(1024 * 1024)

server := grpc.NewServer()

pb.RegisterDepositServiceServer(server, &DepositServer{})

go func() {
if err := server.Serve(listener); err != nil {
log.Fatal(err)
}
}()

return func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}
}

func TestDepositServer_Deposit(t *testing.T) {
tests := []struct {
name string
amount float32
res *pb.DepositResponse
errCode codes.Code
errMsg string
}{
{
"invalid request with negative amount",
-1.11,
nil,
codes.InvalidArgument,
fmt.Sprintf("cannot deposit %v", -1.11),
},
{
"valid request with non negative amount",
0.00,
&pb.DepositResponse{Ok: true},
codes.OK,
"",
},
}

ctx := context.Background()

conn, err := grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithContextDialer(dialer()))
if err != nil {
log.Fatal(err)
}
defer conn.Close()

client := pb.NewDepositServiceClient(conn)

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
request := &pb.DepositRequest{Amount: tt.amount}

response, err := client.Deposit(ctx, request)

if response != nil {
if response.GetOk() != tt.res.GetOk() {
t.Error("response: expected", tt.res.GetOk(), "received", response.GetOk())
}
}

if err != nil {
if er, ok := status.FromError(err); ok {
if er.Code() != tt.errCode {
t.Error("error code: expected", codes.InvalidArgument, "received", er.Code())
}
if er.Message() != tt.errMsg {
t.Error("error message: expected", tt.errMsg, "received", er.Message())
}
}
}
})
}
}

İstemci


Yapı


├── Makefile
├── cmd
│   └── client
│   └── main.go
├── internal
│   └── bank
│   └── account
│   ├── deposit_client.go
│   └── deposit_client_test.go
└── pkg
   └── proto
   └── bank
   └── account
   ├── deposit.pb.go
   └── deposit.proto

Dosyalar


Makefile

.PHONY: compile
compile: ## Compile the proto file.
protoc -I pkg/proto/bank/account/ pkg/proto/bank/account/deposit.proto --go_out=plugins=grpc:pkg/proto/bank/account/

.PHONY: run
run: ## Build and run client.
go build -race -ldflags "-s -w" -o bin/client cmd/client/main.go
bin/client

pkg/proto/bank/account/deposit.proto

syntax = "proto3";

package account;

option go_package = ".;account";

message DepositRequest {
float amount = 1;
}

message DepositResponse {
bool ok = 1;
}

service DepositService {
rpc Deposit(DepositRequest) returns (DepositResponse) {}
}

pkg/proto/bank/account/deposit.pb.go

Bu dosya make compile komutu ile yaratıldığından içeriğini göstermiyorum.


cmd/client/main.go

package main

import (
"context"
"log"
"time"

"github.com/inanzzz/client/internal/bank/account"
"google.golang.org/grpc"
)

func main() {
log.Println("Client running ...")

conn, err := grpc.Dial(":50051", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalln(err)
}
defer conn.Close()

response, err := account.
NewDepositClient(conn, time.Second).
Deposit(context.Background(), 1990.01)

log.Println(response)
log.Println(err)
}

internal/bank/account/deposit_client.go

package account

import (
"context"
"fmt"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/status"

pb "github.com/inanzzz/client/pkg/proto/bank/account"
)

type DepositClient struct {
conn *grpc.ClientConn
timeout time.Duration
}

func NewDepositClient(conn *grpc.ClientConn, timeout time.Duration) DepositClient {
return DepositClient{
conn: conn,
timeout: timeout,
}
}

func (d DepositClient) Deposit(ctx context.Context, amount float32) (bool, error) {
client := pb.NewDepositServiceClient(d.conn)

request := &pb.DepositRequest{Amount: amount}

ctx, cancel := context.WithDeadline(ctx, time.Now().Add(d.timeout))
defer cancel()

response, err := client.Deposit(ctx, request)
if err != nil {
if er, ok := status.FromError(err); ok {
return false, fmt.Errorf("grpc: %s, %s", er.Code(), er.Message())
}
return false, fmt.Errorf("server: %s", err.Error())
}

return response.GetOk(), nil
}

internal/bank/account/deposit_client_test.go

package account

import (
"context"
"errors"
"fmt"
"log"
"net"
"testing"
"time"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"

pb "github.com/inanzzz/client/pkg/proto/bank/account"
)

type mockDepositServer struct {
pb.UnimplementedDepositServiceServer
}

func (*mockDepositServer) Deposit(ctx context.Context, req *pb.DepositRequest) (*pb.DepositResponse, error) {
if req.GetAmount() < 0 {
return nil, status.Errorf(codes.InvalidArgument, "cannot deposit %v", req.GetAmount())
}

return &pb.DepositResponse{Ok: true}, nil
}

func dialer() func(context.Context, string) (net.Conn, error) {
listener := bufconn.Listen(1024 * 1024)

server := grpc.NewServer()

pb.RegisterDepositServiceServer(server, &mockDepositServer{})

go func() {
if err := server.Serve(listener); err != nil {
log.Fatal(err)
}
}()

return func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}
}

func TestDepositClient_Deposit(t *testing.T) {
tests := []struct {
name string
amount float32
res bool
err error
}{
{
"invalid request with negative amount",
-1.11,
false,
fmt.Errorf("grpc: InvalidArgument, cannot deposit %v", -1.11),
},
{
"valid request with non negative amount",
0.00,
true,
nil,
},
}

ctx := context.Background()

conn, err := grpc.DialContext(ctx, "", grpc.WithInsecure(), grpc.WithContextDialer(dialer()))
if err != nil {
log.Fatal(err)
}
defer conn.Close()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
response, err := NewDepositClient(conn, time.Second).Deposit(context.Background(), tt.amount)

if response != tt.res {
t.Error("error: expected", tt.res, "received", response)
}

if err != nil && errors.Is(err, tt.err) {
t.Error("error: expected", tt.err, "received", err)
}
})
}
}

Testler


Sunucu


$ go test -v -run TestDepositServer_Deposit ./internal/bank/account/

=== RUN TestDepositServer_Deposit
=== RUN TestDepositServer_Deposit/invalid_request_with_negative_amount
=== RUN TestDepositServer_Deposit/valid_request_with_non_negative_amount
--- PASS: TestDepositServer_Deposit (0.00s)
--- PASS: TestDepositServer_Deposit/invalid_request_with_negative_amount (0.00s)
--- PASS: TestDepositServer_Deposit/valid_request_with_non_negative_amount (0.00s)
PASS
ok github.com/inanzzz/client/internal/bank/account 0.011s

# When you run with test coverage
coverage: 100.0% of statements

İstemci


$ go test -v -run TestDepositClient_Deposit ./internal/bank/account/

=== RUN TestDepositClient_Deposit
=== RUN TestDepositClient_Deposit/invalid_request_with_negative_amount
=== RUN TestDepositClient_Deposit/valid_request_with_non_negative_amount
--- PASS: TestDepositClient_Deposit (0.00s)
--- PASS: TestDepositClient_Deposit/invalid_request_with_negative_amount (0.00s)
--- PASS: TestDepositClient_Deposit/valid_request_with_non_negative_amount (0.00s)
PASS
ok github.com/inanzzz/client/internal/bank/account 0.011s

# When you run with test coverage
coverage: 90.9% of statements