قسمت دوم از سری مطالب «چطور یک سیستم مایکروسرویس روی کوبرنیتز بسازیم».
کد کامل این پست رو میتونید اینجا ببینید.
gRPC
ما تقریبا در تمامی سرویسهای پلتفرم از gRPC برای ارتباط بین سرویسها استفاده میکنیم. gRPC یک فریمورک بهینه توسعه RPC است که قبلا قابلیتهاش در شرکتهای بزرگ اثبات شده است.
در کنار کارایی gRPC مهمترین دلیلی که ما gRPC رو تقریبا همه جا استفاده میکنیم پروتکل انتقال پیام این فریم ورک یعنی Protocol Buffers است. حتی در سرویسهایی که از gRPC استفاده نمیکنیم، پیامهایی که به سمت سرویسهای داخلی ارسال میکنیم را توسط پروتکلبافر (یا به اختصار پروتوباف) منتقل میکنیم.
اینجا از یک مثال ساده برای نمایش نحوه mock کردن gRPC استفاده میکنم.
ایجاد یک سرویس gRPC
در این مطلب ابتدا با تعریف یک فایل پروتوباف اون رو کامپایل و سپس یک کلاینت برای این سرویس پیادهسازی و در نهایت در تستها این کلاینت gRPC رو mock میکنیم.
تعریف فایل Protobuf
این سرویس از دو rpc تشکیل شده که یکی با دریافت پیام PING پیام PONG رو در جواب بر میگردونه و دومی با دریافت هر پیامی، همون پیام رو در جواب بر میگردونه:
syntax = "proto3";
package pb;
service Signal {
rpc Ping (PingRequest) returns (PingResponse) {}
rpc Echo (EchoRequest) returns (EchoResponse) {}
}
option go_package = ".;pb";
message PingRequest {
string message = 1;
}
message PingResponse {
string message = 1;
}
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
}
کدهای پروتوباف رو کامپایل میکنیم:
protoc -I/opt/include -I/usr/local/include -Ipb \
--go_out=./pb --go_opt=paths=source_relative \
--go-grpc_out=./pb --go-grpc_opt=paths=source_relative \
pb/*.proto
پیادهسازی کلاینت
کلاینت رو به عنوان دو http.HandlerFunc پیاده سازی میکنم:
package signal
import (
"context"
"encoding/json"
"grpc-mock-example/pb"
"log"
"net/http"
"time"
"google.golang.org/grpc/status"
"google.golang.org/grpc"
)
var (
conn *grpc.ClientConn
client pb.SignalClient
)
func New(serverAddr string) pb.SignalClient {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithBlock())
conn, err := grpc.Dial(serverAddr, opts...)
if err != nil {
log.Fatalf("fail to dial: %v", err)
}
client = pb.NewSignalClient(conn)
return client
}
func Ping(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
message := r.URL.Path[len("/ping/"):]
ping, err := client.Ping(ctx, &pb.PingRequest{Message: message})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
e, ok := status.FromError(err)
if ok {
json.NewEncoder(w).Encode(e.Message())
return
}
em := err.Error()
log.Printf("%s", em)
json.NewEncoder(w).Encode(em)
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
json.NewEncoder(w).Encode(ping)
}
func Echo(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
message := r.URL.Path[len("/echo/"):]
echo, err := client.Echo(ctx, &pb.EchoRequest{Message: message})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
e, ok := status.FromError(err)
if ok {
json.NewEncoder(w).Encode(e.Message())
return
}
json.NewEncoder(w).Encode(err.Error())
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
json.NewEncoder(w).Encode(echo)
}
func Close() {
conn.Close()
}
برای تولید mock ها از پکیج gomock استفاده میکنیم:
mockgen -source=pb/signal_grpc.pb.go -package=mocks SignalClient > ./mocks/signal_client_mock.go;
و بالاخره mock کردن gRPC در تستها
package signal
import (
"bytes"
"encoding/json"
"fmt"
"grpc-mock-example/internal"
"grpc-mock-example/mocks"
"grpc-mock-example/pb"
"net/http"
"net/url"
"reflect"
"testing"
"github.com/golang/mock/gomock"
fuzz "github.com/google/gofuzz"
)
type pingRequest struct {
message string
}
func (r *pingRequest) Matches(msg interface{}) bool {
m, ok := msg.(*pb.PingRequest)
if !ok {
return false
}
return m.Message == r.message
}
func (r *pingRequest) String() string {
return fmt.Sprintf("is %s %v", r.message, r.message)
}
type customResponseWriter struct {
header http.Header
body *bytes.Buffer
statusCode int
}
func (c customResponseWriter) Header() http.Header {
return c.header
}
func (c customResponseWriter) Write(body []byte) (int, error) {
return c.body.Write(body)
}
func (c customResponseWriter) WriteHeader(statusCode int) {
c.statusCode = statusCode
}
func TestPing(t *testing.T) {
var msg string
f := fuzz.New()
ctrl := gomock.NewController(t)
signalClient := mocks.NewMockSignalClient(ctrl)
f.Fuzz(&msg)
signalClient.
EXPECT().
Ping(gomock.Any(), &pingRequest{message: "Ping"}, gomock.Any()).
Return(nil, internal.ErrInvalidPing).
AnyTimes()
signalClient.
EXPECT().
Ping(gomock.Any(), &pingRequest{message: "PING"}, gomock.Any()).
Return(&pb.PingResponse{Message: "PONG"}, nil).
AnyTimes()
client = signalClient
type args struct {
w customResponseWriter
r *http.Request
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "Ping",
args: args{
w: customResponseWriter{
header: http.Header{},
body: &bytes.Buffer{},
},
r: &http.Request{
Method: "GET",
URL: &url.URL{
Path: "/ping/Ping",
},
},
},
want: "\"invalid ping message\"\n",
},
{
name: "PING",
args: args{
w: customResponseWriter{
header: http.Header{},
body: &bytes.Buffer{},
},
r: &http.Request{
Method: "GET",
URL: &url.URL{
Path: "/ping/PING",
},
},
},
want: "{\"message\":\"PONG\"}\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Ping(tt.args.w, tt.args.r)
got := tt.args.w.body.String()
if !reflect.DeepEqual(tt.args.w.body.String(), tt.want) {
t.Errorf("TestPing() = %v, want %v", got, tt.want)
}
})
}
}
کد کامل این پست رو میتونید اینجا ببینید.