Тестирование службы gRPC
Я бы хотел проверить службу gRPC, написанную на Go. Пример, который я использую, - это пример сервера Hello World из репозитория grpc-go.
Определение протобуфа выглядит следующим образом:
syntax = "proto3";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
И тип в greeter_server
main:
// server is used to implement helloworld.GreeterServer.
type server struct{}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
Я искал примеры, но не смог найти, как реализовать тесты для службы gRPC в Go.
Ответы
Ответ 1
Если вы хотите убедиться, что выполнение службы gRPC делает то, что вы ожидаете, тогда вы можете просто написать стандартные модульные тесты и полностью игнорировать сетевые процессы.
Например, make greeter_server_test.go
:
func HelloTest(t *testing.T) {
s := server{}
// set up test cases
tests := []struct{
name string
want string
} {
{
name: "world",
want: "Hello world",
},
{
name: "123",
want: "Hello 123",
},
}
for _, tt := range tests {
req := &pb.HelloRequest{Name: tt.name}
resp, err := s.SayHello(context.Background(), req)
if err != nil {
t.Errorf("HelloTest(%v) got unexpected error")
}
if resp.Message != tt.want {
t.Errorf("HelloText(%v)=%v, wanted %v", tt.name, resp.Message, tt.want)
}
}
}
Я, возможно, испортил синтаксис прото, немного делающий это из памяти, но эта идея.
Ответ 2
Я думаю, что вы ищете пакет google.golang.org/grpc/test/bufconn
, который поможет вам не запускать службу с реальным номером порта, но при этом разрешать тестирование потоковых RPC.
import "google.golang.org/grpc/test/bufconn"
const bufSize = 1024 * 1024
var lis *bufconn.Listener
func init() {
lis = bufconn.Listen(bufSize)
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
go func() {
if err := s.Serve(lis); err != nil {
log.Fatalf("Server exited with error: %v", err)
}
}()
}
func bufDialer(string, time.Duration) (net.Conn, error) {
return lis.Dial()
}
func TestSayHello(t *testing.T) {
ctx := context.Background()
conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithDialer(bufDialer), grpc.WithInsecure())
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
resp, err := client.SayHello(ctx, &pb.HelloRequest{"Dr. Seuss"})
if err != nil {
t.Fatalf("SayHello failed: %v", err)
}
log.Printf("Response: %+v", resp)
// Test for output here.
}
Преимущество этого подхода состоит в том, что вы по-прежнему получаете сетевое поведение, но через соединение в памяти без использования ресурсов уровня ОС, таких как порты, которые могут или не могут быстро очиститься. И это позволяет вам протестировать его так, как оно фактически использовалось, и дает вам правильное потоковое поведение.
У меня нет потокового примера с макушки головы, но волшебный соус выше. Это дает вам все ожидаемое поведение обычного сетевого подключения. Хитрость заключается в настройке параметра WithDialer, как показано, с использованием пакета bufconn для создания прослушивателя, который предоставляет свою собственную программу набора номера. Я постоянно использую эту технику для тестирования сервисов gRPC, и она прекрасно работает.
Ответ 3
Возможно, это простой способ просто протестировать потоковое обслуживание. Извиняюсь, если есть какая-либо опечатка, поскольку я адаптирую это из некоторого работающего кода.
Учитывая следующее определение.
rpc ListSites(Filter) returns(stream sites)
Со следующим кодом на стороне сервера.
// ListSites ...
func (s *SitesService) ListSites(filter *pb.SiteFilter, stream pb.SitesService_ListSitesServer) error {
for _, site := range s.sites {
if err := stream.Send(site); err != nil {
return err
}
}
return nil
}
Теперь все, что вам нужно сделать, - это mock pb.SitesService_ListSitesServer в вашем файле тестов.
type mockSiteService_ListSitesServer struct {
grpc.ServerStream
Results []*pb.Site
}
func (_m *mockSiteService_ListSitesServer) Send(site *pb.Site) error {
_m.Results = append(_m.Results, site)
return nil
}
Это отвечает на событие .send и записывает отправленные объекты в. Результаты, которые затем можно использовать в ваших утверждениях.
Наконец, вы вызываете код сервера с издеваемым внедрением pb.SitesService_ListSitesServer.
func TestListSites(t *testing.T) {
s := SiteService.NewSiteService()
filter := &pb.SiteFilter{}
mock := &mockSiteService_ListSitesServer{}
s.ListSites(filter, mock)
assert.Equal(t, 1, len(mock.Results), "Sites expected to contain 1 item")
}
Нет, он не тестирует весь стек, но он позволяет вам проверять ваш код на стороне сервера без хлопот при запуске полноценной службы gRPC либо для реальной, либо в макетной форме.
Ответ 4
Я придумал следующую реализацию, которая не может быть лучшим способом сделать это. В основном, используя функцию TestMain
для TestMain
сервера с помощью goroutine:
const (
port = ":50051"
)
func Server() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
func TestMain(m *testing.M) {
go Server()
os.Exit(m.Run())
}
а затем реализовать клиента в остальных тестах:
func TestMessages(t *testing.T) {
// Set up a connection to the Server.
const address = "localhost:50051"
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
t.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Test SayHello
t.Run("SayHello", func(t *testing.T) {
name := "world"
r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
if err != nil {
t.Fatalf("could not greet: %v", err)
}
t.Logf("Greeting: %s", r.Message)
if r.Message != "Hello "+name {
t.Error("Expected 'Hello world', got ", r.Message)
}
})
}
Ответ 5
BTW: как новый участник, я не могу добавить к комментариям. Поэтому я добавляю здесь новый ответ.
Я могу подтвердить, что подход @Omar работает для тестирования ненагруженной службы gRPC путем тестирования через интерфейс без запуска службы.
Однако этот подход не будет работать для потоков. Поскольку gRPC поддерживает двунаправленные потоки, необходимо запустить службу и подключиться к ней через сетевой уровень для тестирования потоков.
Подход, который @joscas выполняет для потоков gRPC (хотя код примера helloworld не использует потоки), используя goroutine для запуска службы. Тем не менее, я заметил, что в Mac OS X 10.11.6 он не выпускает порт, используемый службой, последовательно при вызове из goroutine (как я понимаю, служба блокирует goroutine и, возможно, не выйдет полностью). Путем запуска отдельного процесса для запуска службы, используя "exec.Command" и убивая ее до завершения, порт выдается последовательно.
Я загрузил рабочий тестовый файл для службы gRPC, используя потоки в github: https://github.com/mmcc007/go/blob/master/examples/route_guide/server/server_test.go
Вы можете увидеть тесты, выполняемые на travis: https://travis-ci.org/mmcc007/go
Пожалуйста, дайте мне знать, если есть какие-либо предложения по улучшению тестирования служб gRPC.
Ответ 6
Существует множество способов проверить службу gRPC. Вы можете выбрать тестирование по-разному, в зависимости от того, какую уверенность вы хотели бы достичь. Вот три случая, которые иллюстрируют некоторые распространенные сценарии.
Случай № 1: я хочу проверить свою бизнес-логику
В этом случае вас интересует логика сервиса и то, как он взаимодействует с другими компонентами. Лучшее, что можно сделать здесь, это написать несколько тестов.
Алекс Эллис предлагает хорошее введение в модульное тестирование в Go. Если вам нужно проверить взаимодействие, то GoMock - это то, что вам нужно. Сергей Гребенщиков написал хороший урок по GoMock.
Ответ от Омара показывает, как вы можете подойти к модульному тестированию этого конкретного примера SayHello
.
Случай № 2: я хочу вручную проверить API моего живого сервиса по проводам
В этом случае вы заинтересованы в ручном поисковом тестировании вашего API. Обычно это делается для изучения реализации, проверки крайних случаев и получения уверенности в том, что ваш API ведет себя так, как ожидалось.
Вам нужно будет:
- Запустите свой сервер gRPC
- Используйте решение для беспроводного моделирования, чтобы смоделировать любые ваши зависимости, например, если ваша тестируемая служба gRPC выполняет вызов gRPC к другой службе. Например, вы можете использовать Traffic Parrot.
- Используйте инструмент тестирования API gRPC. Например, вы можете использовать gRPC CLI.
Теперь вы можете использовать свое решение для моделирования, чтобы моделировать реальные и гипотетические ситуации, наблюдая за поведением тестируемого сервиса с помощью инструмента тестирования API.
Случай № 3: Я хочу автоматизировать проводное тестирование моего API
В этом случае вы заинтересованы в написании автоматических приемочных тестов в стиле BDD, которые взаимодействуют с тестируемой системой через API-интерфейс gRPC. Эти тесты являются дорогостоящими для написания, запуска и обслуживания и должны использоваться экономно, имея в виду пирамиду тестирования.
Ответ от thinkerou показывает, как вы можете использовать karate-grpc для написания этих тестов API в Java. Вы можете комбинировать это с плагином Traffic Parrot Maven, чтобы высмеивать любые зависимости по проводам.
Ответ 7
Вы можете использовать karate-grpc
для тестирования службы grpc, вам нужно только опубликовать свой ip/порт прото-jar и grpc-сервер. karate-grpc
билд на основе каратэ и полиглота.
Один привет-пример:
Feature: grpc helloworld example by grpc dynamic client
Background:
* def Client = Java.type('com.github.thinkerou.karate.GrpcClient')
* def client = Client.create('localhost', 50051)
Scenario: do it
* def payload = read('helloworld.json')
* def response = client.call('helloworld.Greeter/SayHello', payload)
* def response = JSON.parse(response)
* print response
* match response[0].message == 'Hello thinkerou'
* def message = response[0].message
* def payload = read('again-helloworld.json')
* def response = client.call('helloworld.Greeter/AgainSayHello', payload)
* def response = JSON.parse(response)
* match response[0].details == 'Details Hello thinkerou in BeiJing'
О примере карате-grpc комментарий:
И он будет генерировать красивый отчет, как:
Более подробную информацию см. По адресу: https://thinkerou.com/karate-grpc/.