19/07/2020 - GO
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.
go get -u google.golang.org/grpc
# MacOS
brew install protobuf
go get -u github.com/golang/protobuf/protoc-gen-go
go get -u google.golang.org/grpc/test/bufconn
├── Makefile
├── cmd
│ └── server
│ └── main.go
├── internal
│ └── bank
│ └── account
│ ├── deposit_server.go
│ └── deposit_server_test.go
└── pkg
└── proto
└── bank
└── account
├── deposit.pb.go
└── deposit.proto
.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
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) {}
}
Bu dosya make compile
komutu ile yaratıldığından içeriğini göstermiyorum.
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))
}
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
}
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())
}
}
}
})
}
}
├── Makefile
├── cmd
│ └── client
│ └── main.go
├── internal
│ └── bank
│ └── account
│ ├── deposit_client.go
│ └── deposit_client_test.go
└── pkg
└── proto
└── bank
└── account
├── deposit.pb.go
└── deposit.proto
.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
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) {}
}
Bu dosya make compile
komutu ile yaratıldığından içeriğini göstermiyorum.
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)
}
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
}
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)
}
})
}
}
$ 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
$ 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