Тестирование службы 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 ведет себя так, как ожидалось.

Вам нужно будет:

  1. Запустите свой сервер gRPC
  2. Используйте решение для беспроводного моделирования, чтобы смоделировать любые ваши зависимости, например, если ваша тестируемая служба gRPC выполняет вызов gRPC к другой службе. Например, вы можете использовать Traffic Parrot.
  3. Используйте инструмент тестирования 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 комментарий:

enter image description here

И он будет генерировать красивый отчет, как:

enter image description here

Более подробную информацию см. По адресу: https://thinkerou.com/karate-grpc/.