Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to write tests for the gateway? #699

Closed
seanhagen opened this issue Jul 12, 2018 · 3 comments
Closed

How to write tests for the gateway? #699

seanhagen opened this issue Jul 12, 2018 · 3 comments

Comments

@seanhagen
Copy link

I'd like to be able to write some tests to validate that everything that gets auto-generated on the gateway side hasn't changed. Basically, I'd like to write some tests so that I can change things in the proto file but the tests will fail if the gateway doesn't respond ( or throws an error for invalid input ). That way, I don't have to worry about breaking things for the non-grpc clients that use the gateway.

This is what I've got right now ( and it's not working ):

package example

import (
	"bytes"
	"context"
	"fmt"
	"net"
	"net/http/httptest"
	"testing"

	"github.com/import/path/of/generated/code/api"
	"github.com/davecgh/go-spew/spew"
	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"github.com/soheilhy/cmux"
	"google.golang.org/grpc"
)

var testServerListen = ":9999"

func TestGatewayWorks(t *testing.T) {
	ctx := context.Background()

	listen, err := net.Listen("tcp", testServerListen)
	if err != nil {
		t.Errorf("unable to set up listener for test: %v", err)
		t.FailNow()
	}

	tcpMux := cmux.New(listen)
	grpcL := tcpMux.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"))

	conn, err := grpc.DialContext(ctx, testServerListen, grpc.WithInsecure())
	if err != nil {
		t.Errorf("unable to dial: %v", err)
		t.FailNow()
	}

	srv := grpc.NewServer()
	mux := runtime.NewServeMux()

	s := &Service{}
	api.RegisterExampleServer(srv, s)

	err = api.RegisterExampleHandler(ctx, mux, conn)
	if err != nil {
		t.Errorf("unable to register handler: %v", err)
		t.FailNow()
	}

	fmt.Println("starting goroutine")
	go func() {
		fmt.Println("starting grpc server")
		err := srv.Serve(grpcL)
		if err != nil {
			t.Errorf("error while setting up or runing server: %v", err)
			t.FailNow()
		}
	}()
	fmt.Println("here")

	w := httptest.NewRecorder()
	r := httptest.NewRequest("GET", fmt.Sprintf("http://localhost%v/v2/examples", testServerListen), bytes.NewReader(nil))

	mux.ServeHTTP(w, r)

	spew.Dump(w, r)

	srv.GracefulStop()
}

Most of that is copying bits and pieces from how we assemble the grpc/http servers in our service backend library.

The response that comes back:

(*httptest.ResponseRecorder)(0xc420330a00)({                                                               
 Code: (int) 503,                                                                                                                     
 HeaderMap: (http.Header) (len=1) {                 
  (string) (len=12) "Content-Type": ([]string) (len=1 cap=1) {
   (string) (len=16) "application/json"                                                        
  }                                                                                                               
 },                                                                                                                                                   
 Body: (*bytes.Buffer)(0xc42038e930)({"error":"transport is closing","code":14}),
 Flushed: (bool) false,                                                                                                              
 result: (*http.Response)(<nil>),                                                                                             
 snapHeader: (http.Header) (len=1) {                                                                                                            
  (string) (len=12) "Content-Type": ([]string) (len=1 cap=1) {                                                              
   (string) (len=16) "application/json"
  }
 },
 wroteHeader: (bool) true
})

The request:

(*http.Request)(0xc4203e6000)({
 Method: (string) (len=3) "GET",
 URL: (*url.URL)(0xc42015c400)(http://localhost:9999/v2/examples),
 Proto: (string) (len=8) "HTTP/1.1",
 ProtoMajor: (int) 1,
 ProtoMinor: (int) 1,
 Header: (http.Header) {
 },
 Body: (ioutil.nopCloser) {
  Reader: (*bytes.Reader)(0xc420356cf0)({
   s: ([]uint8) <nil>,
   i: (int64) 0,
   prevRune: (int) -1
  })
 },
 GetBody: (func() (io.ReadCloser, error)) <nil>,
 ContentLength: (int64) 0,
 TransferEncoding: ([]string) <nil>,
 Close: (bool) false,
 Host: (string) (len=14) "localhost:9999",
 Form: (url.Values) <nil>,
 PostForm: (url.Values) <nil>,
 MultipartForm: (*multipart.Form)(<nil>),
 Trailer: (http.Header) <nil>,
 RemoteAddr: (string) (len=14) "192.0.2.1:1234",
 RequestURI: (string) (len=36) "http://localhost:9999/v2/examples",
 TLS: (*tls.ConnectionState)(<nil>),
 Cancel: (<-chan struct {}) <nil>,
 Response: (*http.Response)(<nil>),
 ctx: (context.Context) <nil>
})

Also, right now the test doesn't exit automatically, it gets killed at the 10 minute mark by the testing package.

If I could get some pointers on how to accomplish these kinds of tests, that'd be awesome.

@johanbrandhorst
Copy link
Collaborator

I'm going to close this issue since it's not really related to features or bugs of this library. However, having done this kind of testing myself, here are some pointers:

  1. Run your gRPC-Gateway and gRPC server in a goroutine. Maintain a reference to the server so you can call srv.GracefulClose in the main goroutine when your tests are done.
  2. Just use http.Get, http.Post etc. Pretend it's a real webserver. Compare responses with expected JSON.

@asgaines
Copy link

For anyone else stumbling upon this, the 503 Service Unavailable is potentially due to the GRPC server not having fully spun up before a request was launched against it (due to it running within a goroutine).

Besides using a time.Sleep after the goroutine launch (at the fmt.Println("here") line), I haven't come up with a solution to ensure executing code waits for the service's availability

@achew22
Copy link
Collaborator

achew22 commented Sep 13, 2019

I've seen this solved in two other ways:

  1. Put a for loop in that calls the grpc health service repeatedly until success (probably preferred since your service should implement this anyway)
  2. Make a variant of your server that writes to a channel when the service is healthy and wait on that channel.

Best of luck!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants