grpcsteps

package module
v0.12.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 13, 2023 License: MIT Imports: 21 Imported by: 0

README

Cucumber gRPC steps for Golang

GitHub Releases Build Status codecov Go Report Card GoDevDoc

grpcsteps uses go.nhat.io/grpcmock to provide steps for cucumber/godog and makes it easy to run tests with grpc server and client.

Table of Contents

Prerequisites

  • Go >= 1.19

[table of contents]

Usage

Mock external gPRC Services

This is for describing behaviors of gRPC endpoints that are called by the app during test (e.g. 3rd party APIs). The mock creates an gRPC server for each of registered services and allows control of expected requests and responses with gherkin steps.

In simple case, you can define the expected method and response.

Feature: Get Item

    Scenario: Success
        Given "item-service" receives a grpc request "/grpctest.ItemService/GetItem" with payload:
        """
        {
            "id": 42
        }
        """

        And the grpc service responds with payload:
        """
        {
            "id": 42,
            "name": "Item #42"
        }
        """

        # Your application call.

For starting, initiate a server and register it to the scenario.

package mypackage

import (
	"bytes"
	"math/rand"
	"testing"

	"github.com/cucumber/godog"

	"github.com/godogx/grpcsteps"
)

func TestIntegration(t *testing.T) {
	out := bytes.NewBuffer(nil)

	// Create a new grpc servers manager
	m := grpcsteps.NewExternalServiceManager()

	// Setup the 3rd party services here.

	suite := godog.TestSuite{
		Name: "Integration",
		TestSuiteInitializer: func(sc *godog.TestSuiteContext) {
			sc.AfterSuite(func() {
				m.Close()
			})
		},
		ScenarioInitializer: func(sc *godog.ScenarioContext) {
			m.RegisterContext(sc)
		},
		Options: &godog.Options{
			Strict:    true,
			Output:    out,
			Randomize: rand.Int63(),
		},
	}

	// Run the suite.
	if status := suite.Run(); status != 0 {
		t.Fatal(out.String())
	}
}

[table of contents]

Setup

In order to mock the gPRC server, you have to register it to the manager with AddService() while initializing. The first argument is the service ID, the second argument is the function that prototool generates for you. Something like this:

package mypackage

import "google.golang.org/grpc"

func RegisterItemServiceServer(s grpc.ServiceRegistrar, srv ItemServiceServer) {
	s.RegisterService(&ItemService_ServiceDesc, srv)
}

For example:

package mypackage

import (
	"testing"

	"github.com/godogx/grpcsteps"
)

func TestIntegration(t *testing.T) {
	// Create a new grpc servers manager.
	m := grpcsteps.NewExternalServiceManager()

	itemServiceAddr := m.AddService("item-service", RegisterItemServiceServer)

	// itemServiceAddr is going to be something like "[::]:52299".
	// Use that addr for the client in the application.

	// Run test suite.
}

By default, the manager spins up a gRPC with a random port. If you don't like that, you can specify the one you like with grpcmock.WithPort(). For example:

package mypackage

import (
	"testing"

	"go.nhat.io/grpcmock"

	"github.com/godogx/grpcsteps"
)

func TestIntegration(t *testing.T) {
	// Create a new grpc servers manager
	m := grpcsteps.NewExternalServiceManager()

	itemServiceAddr := m.AddService("item-service", RegisterItemServiceServer,
		grpcmock.WithPort(9000),
	)

	// itemServiceAddr is "[::]:9000".

	// Run test suite.
}

You can also use a listener, for example bufconn

package mypackage

import (
	"testing"

	"go.nhat.io/grpcmock"
	"google.golang.org/grpc/test/bufconn"

	"github.com/godogx/grpcsteps"
)

func TestIntegration(t *testing.T) {
	buf := bufconn.Listen(1024 * 1024)

	// Create a new grpc servers manager
	m := grpcsteps.NewExternalServiceManager()

	m.AddService("item-service", RegisterItemServiceServer,
		grpcmock.WithListener(buf),
	)

	// In this case, use the `buf` to connect to server

	// Run test suite.
}

[table of contents]

Steps
Prepare for a request

Mock a new request with (one of) these patterns

  • ^"([^"]*)" receives [a1] (?:gRPC|GRPC|grpc) request "([^"]*)"$
  • ^"([^"]*)" receives [a1] (?:gRPC|GRPC|grpc) request "([^"]*)" with payload:$
  • ^"([^"]*)" receives [a1] (?:gRPC|GRPC|grpc) request "([^"]*)" with payload from file "([^"]+)"$
  • ^"([^"]*)" receives [a1] (?:gRPC|GRPC|grpc) request "([^"]*)" with payload from file:$

Or, if the service receives multiple requests with the same condition, you could use

  • ^"([^"]*)" receives ([0-9]+) (?:gRPC|GRPC|grpc) requests "([^"]*)"$
  • ^"([^"]*)" receives ([0-9]+) (?:gRPC|GRPC|grpc) requests "([^"]*)" with payload:$
  • ^"([^"]*)" receives ([0-9]+) (?:gRPC|GRPC|grpc) requests "([^"]*)" with payload from file "([^"]+)"$
  • ^"([^"]*)" receives ([0-9]+) (?:gRPC|GRPC|grpc) requests "([^"]*)" with payload from file:$

Or, if you don't know how many times it's going to be, use

  • ^"([^"]*)" receives (?:some|many|several) (?:gRPC|GRPC|grpc) requests "([^"]*)"$
  • ^"([^"]*)" receives (?:some|many|several) (?:gRPC|GRPC|grpc) requests "([^"]*)" with payload:$
  • ^"([^"]*)" receives (?:some|many|several) (?:gRPC|GRPC|grpc) requests "([^"]*)" with payload from file "([^"]+)"$
  • ^"([^"]*)" receives (?:some|many|several) (?:gRPC|GRPC|grpc) requests "([^"]*)" with payload from file:$

And, Optionally, you can:

  • Add a header to the request with
    ^The (?:gRPC|GRPC|grpc) request has(?: a)? header "([^"]*): ([^"]*)"$

For example:

Feature: Get Item

    Scenario: Get item with locale
        Given "item-service" receives a grpc request "/grpctest.ItemService/GetItem" with payload:
        """
        {
            "id": 42
        }
        """
        And The gRPC request has a header "Locale: en-US"

        # Your application call.

Note, you can use "<ignore-diff>" in the payload to tell the assertion to ignore a JSON field. For example:

Feature: Create Items

    Scenario: Create items
        Given "item-service" receives a grpc request "/grpctest.ItemService/CreateItems" with payload:
        """
        {
            "id": 42
            "name": "<ignore-diff>",
            "category": "<ignore-diff>",
            "metadata": "<ignore-diff>"
        }
        """

        And the gRPC service responds with payload:
        """
        {
            "num_items": 1
        }
        """

        # The application call.
        When my application receives a request to create some items:
        """
        [
            {
                "id": 42
                "name": "Item #42",
                "category": 40,
                "metadata": {
                    "tags": ["soup"]
                }
            }
        ]
        """

        Then 1 item is created

"<ignore-diff>" can ignore any types, not just string.

[table of contents]

Response
  • Respond OK with payload
    ^[tT]he (?:gRPC|GRPC|grpc) service responds with payload:?$
    ^[tT]he (?:gRPC|GRPC|grpc) service responds with payload from file "([^"]+)"$
    ^[tT]he (?:gRPC|GRPC|grpc) service responds with payload from file:$
  • Response with code and error message
    ^[tT]he (?:gRPC|GRPC|grpc) service responds with code "([^"]*)"$
    ^[tT]he (?:gRPC|GRPC|grpc) service responds with error (?:message )?"([^"]*)"$
    ^[tT]he (?:gRPC|GRPC|grpc) service responds with code "([^"]*)" and error (?:message )?"([^"]*)"$

    If your error message contains quotes ", better use these with a doc string
    ^[tT]he (?:gRPC|GRPC|grpc) service responds with error(?: message)?:$
    ^[tT]he (?:gRPC|GRPC|grpc) service responds with code "([^"]*)" and error(?: message)?:$

For example:

Feature: Create Items

    Scenario: Create items
        Given "item-service" receives a gRPC request "/grpctest.ItemService/CreateItems" with payload:
        """
        [
            {
                "id": 42,
                "name": "Item #42"
            },
            {
                "id": 43,
                "name": "Item #42"
            }
        ]
        """

        And the gRPC service responds with payload:
        """
        {
            "num_items": 2
        }
        """

        # Your application call.

or

Feature: Create Items

    Scenario: Create items
        Given "item-service" receives a gRPC request "/grpctest.ItemService/CreateItems" with payload:
        """
        [
            {
                "id": 42,
                "name": "Item #42"
            },
            {
                "id": 43,
                "name": "Item #42"
            }
        ]
        """

        And the gRPC service responds with code "InvalidArgument" and error "Invalid ID #42"

[table of contents]

Test a gPRC Server.

Initiate a client and register it to the scenario.

package mypackage

import (
	"bytes"
	"math/rand"
	"testing"

	"github.com/cucumber/godog"
	"google.golang.org/grpc"

	"github.com/godogx/grpcsteps"
)

func TestIntegration(t *testing.T) {
	out := bytes.NewBuffer(nil)

	// Create a new grpc client.
	c := grpcsteps.NewClient(
		grpcsteps.RegisterService(
			grpctest.RegisterItemServiceServer,
			grpcsteps.WithDialOptions(
				grpc.WithInsecure(),
			),
		),
	)

	suite := godog.TestSuite{
		Name:                 "Integration",
		TestSuiteInitializer: nil,
		ScenarioInitializer: func(ctx *godog.ScenarioContext) {
			// Register the client.
			c.RegisterContext(ctx)
		},
		Options: &godog.Options{
			Strict:    true,
			Output:    out,
			Randomize: rand.Int63(),
		},
	}

	// Run the suite.
	if status := suite.Run(); status != 0 {
		t.Fatal(out.String())
	}
}

[table of contents]

Setup

In order to test the gPRC server, you have to register it to the client with grpcsteps.RegisterService() while initializing. The first argument is the function that prototool generates for you. Something like this:

package mypackage

import "google.golang.org/grpc"

func RegisterItemServiceServer(s grpc.ServiceRegistrar, srv ItemServiceServer) {
	s.RegisterService(&ItemService_ServiceDesc, srv)
}

You can configure how the client connects to the server by putting the options. For example:

package mypackage

import "google.golang.org/grpc"

func createClient() *grpcsteps.Client {
	return grpcsteps.NewClient(
		grpcsteps.RegisterService(
			grpctest.RegisterItemServiceServer,
			grpcsteps.WithDialOptions(
				grpc.WithInsecure(),
			),
		),
	)
}

If you have multiple services and want to apply a same set of options to all, use grpcsteps.WithDefaultServiceOptions(). For example:

package mypackage

import "google.golang.org/grpc"

func createClient() *grpcsteps.Client {
	return grpcsteps.NewClient(
		// Set default service options.
		grpcsteps.WithDefaultServiceOptions(
			grpcsteps.WithDialOptions(
				grpc.WithInsecure(),
			),
		),
		// Register other services after this.
		grpcsteps.RegisterService(grpctest.RegisterItemServiceServer),
	)
}

[table of contents]

Options

The options are:

  • grpcsteps.WithAddressProvider(interface{Addr() net.Addr}): Connect to the server using the given address provider, the golang's built-in *net.Listener is an address provider.
  • grpcsteps.WithAddr(string): Connect to the server using the given address. For example: :9090 or localhost:9090.
  • grpcsteps.WithDialOption(grpc.DialOption): Add a dial option for connecting to the server.
  • grpcsteps.WithDialOptions(...grpc.DialOption): Add multiple dial options for connecting to the server.

[table of contents]

Steps
Prepare for a request

Create a new request with (one of) these patterns

  • ^I request(?: a)? (?:gRPC|GRPC|grpc)(?: method)? "([^"]*)" with payload:?$
  • ^I request(?: a)? (?:gRPC|GRPC|grpc)(?: method)? "([^"]*)" with payload from file "([^"]+)"$
  • ^I request(?: a)? (?:gRPC|GRPC|grpc)(?: method)? "([^"]*)" with payload from file:$

Optionally, you can:

  • Add a header to the request with
    ^The (?:gRPC|GRPC|grpc) request has(?: a)? header "([^"]*): ([^"]*)"$
  • Set a timeout for the request with
    ^The (?:gRPC|GRPC|grpc) request timeout is "([^"]*)"$

For example:

Feature: Get Item

    Scenario: Get item with locale
        When I request a gRPC method "/grpctest.ItemService/GetItem" with payload:
        """
        {
            "id": 42
        }
        """
        And The gRPC request has a header "Locale: en-US"

[table of contents]

Execute the request and validate the result.
  • Check only the response code
    ^I should have(?: a)? (?:gRPC|GRPC|grpc) response with code "([^"]*)"$
  • Check if the request is successful and the response payload matches an expectation
    ^I should have(?: a)? (?:gRPC|GRPC|grpc) response with payload:?$
    ^I should have(?: a)? (?:gRPC|GRPC|grpc) response with payload from file "([^"]+)"$
    ^I should have(?: a)? (?:gRPC|GRPC|grpc) response with payload from file:$
  • Check for error code and error message
    ^I should have(?: a)? (?:gRPC|GRPC|grpc) response with error (?:message )?"([^"]*)"$
    ^I should have(?: a)? (?:gRPC|GRPC|grpc) response with code "([^"]*)" and error (?:message )?"([^"]*)"$

    If your error message contains quotes ", better use these with a doc string
    ^I should have(?: a)? (?:gRPC|GRPC|grpc) response with error (?:message )?:$
    ^I should have(?: a)? (?:gRPC|GRPC|grpc) response with code "([^"]*)" and error (?:message )?:$

For example:

Feature: Create Items

    Scenario: Create items
        When I request a gRPC method "/grpctest.ItemService/CreateItems" with payload:
        """
        [
            {
                "id": 42,
                "name": "Item #42"
            },
            {
                "id": 43,
                "name": "Item #42"
            }
        ]
        """

        Then I should have a gRPC response with payload:
        """
        {
            "num_items": 2
        }
        """

or

Feature: Create Items

    Scenario: Create items
        When I request a gRPC method "/grpctest.ItemService/CreateItems" with payload:
        """
        [
            {
                "id": 42,
                "name": "Item #42"
            },
            {
                "id": 43,
                "name": "Item #42"
            }
        ]
        """

        Then I should have a gRPC response with error:
        """
        invalid "id"
        """

[table of contents]

Documentation

Overview

Package grpcsteps provides.

Index

Constants

View Source
const (
	// ErrInvalidGRPCMethod indicates that the service method is not in SERVICE/METHOD format.
	ErrInvalidGRPCMethod err = `invalid grpc method`
	// ErrGRPCServiceNotFound indicates that the service is not found.
	ErrGRPCServiceNotFound err = `grpc service not found`
	// ErrGRPCMethodNotFound indicates that the service method is not found.
	ErrGRPCMethodNotFound err = `grpc method not found`
	// ErrGRPCMethodNotSupported indicates that the service method is not supported.
	ErrGRPCMethodNotSupported err = `grpc method not supported`
)
View Source
const ErrNoClientRequestInContext err = "no client request in context"

ErrNoClientRequestInContext indicates that there is no client request in context.

View Source
const ErrNoRequestPlannerInContext err = "no request planner in context"

ErrNoRequestPlannerInContext indicates that there is no request planner in context.

View Source
const ErrNoServiceRequestInContext err = "no service request in context"

ErrNoServiceRequestInContext indicates that there is no service request in context.

Variables

This section is empty.

Functions

This section is empty.

Types

type AddrProvider

type AddrProvider interface {
	Addr() net.Addr
}

AddrProvider provides a net address.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a grpc client for godog.

func NewClient

func NewClient(opts ...ClientOption) *Client

NewClient initiates a new grpc server extension for testing.

func (*Client) RegisterContext

func (c *Client) RegisterContext(sc *godog.ScenarioContext)

RegisterContext registers to godog scenario.

type ClientOption

type ClientOption func(s *Client)

ClientOption sets up a client.

func RegisterService

func RegisterService(registerFunc interface{}, opts ...ServiceOption) ClientOption

RegisterService registers a grpc server by its interface.

func RegisterServiceFromInstance

func RegisterServiceFromInstance(id string, svc interface{}, opts ...ServiceOption) ClientOption

RegisterServiceFromInstance registers a grpc server by its interface.

func WithDefaultServiceOptions

func WithDefaultServiceOptions(opts ...ServiceOption) ClientOption

WithDefaultServiceOptions set default service options.

type ExternalServiceManager added in v0.5.0

type ExternalServiceManager struct {
	// contains filtered or unexported fields
}

ExternalServiceManager is a grpc server for godog.

func NewExternalServiceManager added in v0.5.0

func NewExternalServiceManager() *ExternalServiceManager

NewExternalServiceManager initiates a new external service manager for testing.

func (*ExternalServiceManager) AddService added in v0.5.0

func (m *ExternalServiceManager) AddService(id string, opts ...grpcmock.ServerOption) string

AddService starts a new service and returns the server address for client to connect.

func (*ExternalServiceManager) Close added in v0.5.0

func (m *ExternalServiceManager) Close()

Close closes the server.

func (*ExternalServiceManager) RegisterContext added in v0.5.0

func (m *ExternalServiceManager) RegisterContext(sc *godog.ScenarioContext)

RegisterContext registers to godog scenario.

type Service

type Service struct {
	service.Method

	Address     string
	DialOptions []grpc.DialOption
}

Service contains needed information to form a GRPC request.

type ServiceOption

type ServiceOption func(s *Service)

ServiceOption sets up a service.

func WithAddr

func WithAddr(addr string) ServiceOption

WithAddr sets service address.

func WithAddressProvider

func WithAddressProvider(p AddrProvider) ServiceOption

WithAddressProvider sets service address.

func WithDialOption

func WithDialOption(o grpc.DialOption) ServiceOption

WithDialOption adds a dial option.

func WithDialOptions

func WithDialOptions(opts ...grpc.DialOption) ServiceOption

WithDialOptions sets dial options.

Directories

Path Synopsis
internal
test
Package test provides functionalities for testing the project.
Package test provides functionalities for testing the project.
test/grpctest
Package grpctest provides the test implementation for the grpctest Service.
Package grpctest provides the test implementation for the grpctest Service.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL