Skip to content

Commit

Permalink
Groundbreaking commit
Browse files Browse the repository at this point in the history
  • Loading branch information
nhatthm committed Oct 28, 2021
1 parent b035d73 commit 25cae22
Show file tree
Hide file tree
Showing 12 changed files with 490 additions and 114 deletions.
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
insert_final_newline = true
max_line_length = 160
tab_width = 4
trim_trailing_whitespace = true

[Makefile]
indent_style = space

[*.feature]
indent_style = space
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
uses: golangci/[email protected]
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.42.0
version: v1.42.1

# Optional: working directory, useful for monorepos
# working-directory: somedir
Expand Down
1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ linters:
- forcetypeassert
- gci
- gochecknoglobals
- golint
- gomnd
- ifshort
- interfacer
Expand Down
53 changes: 38 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,56 @@
# @nhatthm/{name}

<!--
[![GitHub Releases](https://img.shields.io/github/v/release/nhatthm/{name})](https://github.com/nhatthm/{name}/releases/latest)
[![Build Status](https://github.com/nhatthm/{name}/actions/workflows/test.yaml/badge.svg)](https://github.com/nhatthm/{name}/actions/workflows/test.yaml)
[![codecov](https://codecov.io/gh/nhatthm/{name}/branch/master/graph/badge.svg?token=eTdAgDE2vR)](https://codecov.io/gh/nhatthm/{name})
[![Go Report Card](https://goreportcard.com/badge/github.com/nhatthm/{name})](https://goreportcard.com/report/github.com/nhatthm/{name})
[![GoDevDoc](https://img.shields.io/badge/dev-doc-00ADD8?logo=go)](https://pkg.go.dev/github.com/nhatthm/{name})
# gRPC Test Utilities for Golang

[![GitHub Releases](https://img.shields.io/github/v/release/nhatthm/grpcmock)](https://github.com/nhatthm/grpcmock/releases/latest)
[![Build Status](https://github.com/nhatthm/grpcmock/actions/workflows/test.yaml/badge.svg)](https://github.com/nhatthm/grpcmock/actions/workflows/test.yaml)
[![codecov](https://codecov.io/gh/nhatthm/grpcmock/branch/master/graph/badge.svg?token=eTdAgDE2vR)](https://codecov.io/gh/nhatthm/grpcmock)
[![Go Report Card](https://goreportcard.com/badge/github.com/nhatthm/grpcmock)](https://goreportcard.com/report/github.com/nhatthm/grpcmock)
[![GoDevDoc](https://img.shields.io/badge/dev-doc-00ADD8?logo=go)](https://pkg.go.dev/github.com/nhatthm/grpcmock)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?hosted_button_id=PJZSGJN57TDJY)
-->

TBD
Test gRPC service and client like a pro.

## Prerequisites

- `Go >= 1.15`
- `Go >= 1.16`

## Install

```bash
go get github.com/nhatthm/{name}
go get github.com/nhatthm/grpcmock
```

## Usage

TBD
### Invoke a gRPC method

#### Unary Method

```go
package main

import (
"context"
"time"

## Examples
"github.com/nhatthm/grpcmock"
"google.golang.org/grpc/test/bufconn"
)

TBA
func getItem(l *bufconn.Listener, id int32) (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
defer cancel()

out := &Item{}
err := grpcmock.InvokeUnary(ctx, "myservice/GetItem",
&GetItemRequest{Id: id}, out,
grpcmock.WithHeader("Locale", "en-US"),
grpcmock.WithBufConnDialer(l),
grpcmock.WithInsecure(),
)

return out, err
}
```

## Donation

Expand Down
138 changes: 138 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package grpcmock

import (
"context"
"fmt"
"net"
"net/url"
"strings"

"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/test/bufconn"
)

// ContextDialer is to set up the dialer.
type ContextDialer = func(context.Context, string) (net.Conn, error)

type invokeConfig struct {
header map[string]string
dialOpts []grpc.DialOption
callOpts []grpc.CallOption
}

// InvokeOption sets invoker config.
type InvokeOption func(c *invokeConfig)

// InvokeUnary invokes a unary method.
func InvokeUnary(
ctx context.Context,
method string,
in interface{},
out interface{},
opts ...InvokeOption,
) error {
addr, method, err := parseMethod(method)
if err != nil {
return fmt.Errorf("coulld not parse method url: %w", err)
}

ctx, dialOpts, callOpts := invokeOptions(ctx, opts...)

conn, err := grpc.DialContext(ctx, addr, dialOpts...)
if err != nil {
return err
}

return conn.Invoke(ctx, method, in, out, callOpts...)
}

func parseMethod(method string) (string, string, error) {
u, err := url.Parse(method)
if err != nil {
return "", "", err
}

method = fmt.Sprintf("/%s", strings.TrimLeft(u.Path, "/"))

if method == "/" {
return "", "", ErrMissingMethod
}

addr := url.URL{
Scheme: u.Scheme,
User: u.User,
Host: u.Host,
}

return addr.String(), method, nil
}

func invokeOptions(ctx context.Context, opts ...InvokeOption) (context.Context, []grpc.DialOption, []grpc.CallOption) {
cfg := invokeConfig{
header: map[string]string{},
}

for _, o := range opts {
o(&cfg)
}

if len(cfg.header) > 0 {
ctx = metadata.NewOutgoingContext(ctx, metadata.New(cfg.header))
}

return ctx, cfg.dialOpts, cfg.callOpts
}

// WithHeader sets request header.
func WithHeader(key, value string) InvokeOption {
return func(c *invokeConfig) {
c.header[key] = value
}
}

// WithHeaders sets request header.
func WithHeaders(header map[string]string) InvokeOption {
return func(c *invokeConfig) {
for k, v := range header {
c.header[k] = v
}
}
}

// WithContextDialer sets a context dialer to create connections.
//
// See:
// - grpcmock.WithBufConnDialer()
func WithContextDialer(d ContextDialer) InvokeOption {
return WithDialOptions(grpc.WithContextDialer(d))
}

// WithBufConnDialer sets a *bufconn.Listener as the context dialer.
//
// See:
// - grpcmock.WithContextDialer()
func WithBufConnDialer(l *bufconn.Listener) InvokeOption {
return WithContextDialer(func(context.Context, string) (net.Conn, error) {
return l.Dial()
})
}

// WithInsecure disables transport security for the connections.
func WithInsecure() InvokeOption {
return WithDialOptions(grpc.WithInsecure())
}

// WithDialOptions sets dial options.
func WithDialOptions(opts ...grpc.DialOption) InvokeOption {
return func(c *invokeConfig) {
c.dialOpts = append(c.dialOpts, opts...)
}
}

// WithCallOption sets call options.
func WithCallOption(opts ...grpc.CallOption) InvokeOption {
return func(c *invokeConfig) {
c.callOpts = append(c.callOpts, opts...)
}
}
64 changes: 64 additions & 0 deletions client_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package grpcmock

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseMethod(t *testing.T) {
t.Parallel()

testCases := []struct {
scenario string
method string
expectedAddr string
expectedMethod string
expectedError string
}{
{
scenario: "method is not valid",
method: "://",
expectedError: `parse "://": missing protocol scheme`,
},
{
scenario: "method is missing",
method: "tcp://:8000/",
expectedError: `missing method`,
},
{
scenario: "full url",
method: "tcp://127.0.0.1:8000/server/GetItem",
expectedAddr: "tcp://127.0.0.1:8000",
expectedMethod: "/server/GetItem",
},
{
scenario: "method only",
method: "/server/GetItem",
expectedMethod: "/server/GetItem",
},
{
scenario: "relative method",
method: "server/GetItem",
expectedMethod: "/server/GetItem",
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.scenario, func(t *testing.T) {
t.Parallel()

addr, method, err := parseMethod(tc.method)

assert.Equal(t, tc.expectedAddr, addr)
assert.Equal(t, tc.expectedMethod, method)

if tc.expectedError == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.expectedError)
}
})
}
}
60 changes: 60 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package grpcmock_test

import (
"context"
"errors"
"net"
"testing"

"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"

"github.com/nhatthm/grpcmock"
)

func TestInvokeUnary_MethodError(t *testing.T) {
t.Parallel()

err := grpcmock.InvokeUnary(context.Background(), "://", nil, nil)
expected := `coulld not parse method url: parse "://": missing protocol scheme`

assert.EqualError(t, err, expected)
}

func TestInvokeUnary_DialError(t *testing.T) {
t.Parallel()

dialer := func(context.Context, string) (net.Conn, error) {
return nil, errors.New("dial error")
}

err := grpcmock.InvokeUnary(context.Background(), "NotFound", nil, nil,
grpcmock.WithContextDialer(dialer),
grpcmock.WithInsecure(),
)
expected := `rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial error"`

assert.EqualError(t, err, expected)
}

func TestInvokeUnary_Unimplemented(t *testing.T) {
t.Parallel()

l := bufconn.Listen(1024 * 1024)

srv := grpc.NewServer()
defer srv.Stop()

go func() {
_ = srv.Serve(l) // nolint: errcheck
}()

err := grpcmock.InvokeUnary(context.Background(), "grpctest/GetItem", nil, nil,
grpcmock.WithBufConnDialer(l),
grpcmock.WithInsecure(),
)
expected := `rpc error: code = Unimplemented desc = unknown service grpctest`

assert.EqualError(t, err, expected)
}
4 changes: 2 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Package main
package main
// Package grpcmock provides functionalities for testing grpc client and server.
package grpcmock
13 changes: 13 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package grpcmock

const (
// ErrMissingMethod indicates that there is no method in the url.
ErrMissingMethod err = "missing method"
)

type err string

// Error returns the error string.
func (e err) Error() string {
return string(e)
}
Loading

0 comments on commit 25cae22

Please sign in to comment.