Giter Site home page Giter Site logo

deal-go's Introduction

Deal - Go

test codecov Go Report Card

Introduction

This plugin allows us to write Consumer-Driver Contracts tests!

Deal generates some code for us:

  • A Client to be used in the client side to mock the responses based on the contract
  • A Stub Server to be used in the client side as the Client above, but you should run it as another application
  • Server Test Function, where you should pass your server implementation to the function and all the contracts will be validated against it

You can check out an example project here.

Installation

Assuming that you are using Go Modules, it's recommended to use a tool dependency in order to track your tools version:

//go:build tools
// +build tools

package tools

import (
    _ "github.com/faunists/deal-go/protoc-gen-go-deal"
    _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
    _ "google.golang.org/protobuf/cmd/protoc-gen-go"
)

Once you have added the required packages run go mod tidy to resolve the versions and then install them by running:

go install \
    github.com/faunists/deal-go/protoc-gen-go-deal \
    google.golang.org/protobuf/cmd/protoc-gen-go \
    google.golang.org/grpc/cmd/protoc-gen-go-grpc

Usage example

Proto service

First you need a proto service:

syntax = "proto3";

import "google/protobuf/struct.proto";
import "deal/v1/contract/annotations.proto";

option go_package = "YOUR_PACKAGE_HERE/example";

message RequestMessage {
  string requestField = 1;
}

message ResponseMessage {
  int64 responseField = 1;
}

service MyService {
  rpc MyMethod(RequestMessage) returns (ResponseMessage);
}

Contract file

After that you need to write the contract that should be respected, the contract file can be written using a JSON or YAML file. You can set both, Success and Failures cases:

JSON Contract
{
  "name": "Some Name Here",
  "services": {
    "MyService": {
      "MyMethod": {
        "successCases": [
          {
            "description": "Should do something",
            "request": {
              "requestField": "VALUE"
            },
            "response": {
              "responseField": 42
            }
          }
        ],
        "failureCases": [
          {
            "description": "Some description here",
            "request": {
              "requestField": "ANOTHER_VALUE"
            },
            "error": {
              "errorCode": "NotFound",
              "message": "ANOTHER_VALUE NotFound"
            }
          }
        ]
      }
    }
  }
}
YAML Contract
name: Some Name Here
services:
  MyService:
    MyMethod:
      successCases:
        - description: Should do something
          request:
            requestField: VALUE
            someMessage:
              firstField: FIRST_FIELD_VALUE
            someEnum: TWO
          response:
            responseField: 42
            myList:
              - firstField: first
              - firstField: second
            intList: [1, 2, 3]
      failureCases:
        - description: Some description here
          request:
            requestField: ANOTHER_VALUE
          error:
            errorCode: NotFound
            message: ANOTHER_VALUE NotFound"

Generating code

If you're using buf just add the following entries to buf.gen.yaml and execute buf generate passing your contract file path:

version: v1beta1
plugins:
  - name: go
    out: protogen
    opt: paths=source_relative
  - name: go-grpc
    out: protogen
    opt: paths=source_relative
  - name: go-deal
    out: protogen
    opt:
      - paths=source_relative
      - contract-file=contract.yml

Disclaimer: You must be using go-grpc in order to make the things work

Using generated client on tests

Here is an example using the generated client, in the example we're using it inside a test, but it could be used anywhere!

package main_test

import (
	"context"
	"testing"

	"github.com/faunists/deal-go-example/protogen/proto/example"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
)

func TestClient(t *testing.T) {
	t.Run("should return a response", func(t *testing.T) {
		ctx := context.Background()
		expectedResp := &example.ResponseMessage{ResponseField: 42}
		// Generated client
		client := example.MyServiceContractClient{}

		actualResp, err := client.MyMethod(ctx, &example.RequestMessage{
			RequestField: "VALUE",
		})

		require.NoError(t, err)
		require.True(t, proto.Equal(expectedResp, actualResp))
	})

	t.Run("should return an error", func(t *testing.T) {
		ctx := context.Background()
		expectedError := status.Error(codes.NotFound, "ANOTHER_VALUE NotFound")
		// Generated client
		client := example.MyServiceContractClient{}

		_, err := client.MyMethod(ctx, &example.RequestMessage{
			RequestField: "ANOTHER_VALUE",
		})

		require.EqualError(t, err, expectedError.Error())
	})
}

Stub Server (Mock Server)

Deal generates a stub server that you can run it a test against it.

package main

import (
	"log"
	"net"

	"github.com/faunists/deal-go-example/protogen/proto/example"
	"google.golang.org/grpc"
)

func main() {
	// Generated stub server
	stubServer := example.MyServiceStubServer{}

	listener, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("error opening the listener: %v", err)
	}
	defer func() { _ = listener.Close() }()

	grpcServer := grpc.NewServer()
	example.RegisterMyServiceServer(grpcServer, &stubServer)

	log.Printf("grpc server listening at %v", listener.Addr())
	if err = grpcServer.Serve(listener); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
	grpcServer.GracefulStop()
}

Validating contract with server

The first step is to implement our server, the below implementation is compliant with the presented contract:

package server

import (
	"context"

	"github.com/faunists/deal-go-example/protogen/proto/example"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type MyServer struct {
	example.UnimplementedMyServiceServer
}

func (svc *MyServer) MyMethod(
	_ context.Context,
	req *example.RequestMessage,
) (*example.ResponseMessage, error) {
	if req.RequestField == "ANOTHER_VALUE" {
		return nil, status.Error(codes.NotFound, "ANOTHER_VALUE NotFound")
	}

	return &example.ResponseMessage{ResponseField: 42}, nil
}

Now we can use the generated test function that will validate our implementation:

package main_test

import (
	"context"
	"testing"

	"github.com/faunists/deal-go-example/protogen/proto/example"

	"github.com/faunists/deal-go-example/api/server"
	"google.golang.org/grpc"
)

func TestMyServiceContract(t *testing.T) {
	grpcServer := grpc.NewServer()
	example.RegisterMyServiceServer(grpcServer, &server.MyServer{})

	// Testing the implementation
	example.MyServiceContractTest(t, context.Background(), grpcServer)
}

deal-go's People

Contributors

florkbr avatar thepabloaguilar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

florkbr

deal-go's Issues

Support `protoreflect.Message` field type

We need to support the protoreflect.Message in our processor field function that formats the message to its string representation!

  • Make the function works correctly withprotoreflect.Message
  • Write tests to stress as many possibilities as possible

Support `protoreflect.List` field type

We need to support the protoreflect.List in our processor field function that formats the message to its string representation!

  • Make the function works correctly withprotoreflect.List
  • Write tests to stress as many possibilities as possible

Support `protoreflect.EnumNumber` field type

We need to support the protoreflect.EnumNumber in our processor field function that formats the message to its string representation!

  • Make the function works correctly withprotoreflect.EnumNumber
  • Write tests to stress as many possibilities as possible

Support `protoreflect.Map` field type

We need to support the protoreflect.Map in our processor field function that formats the message to its string representation!

  • Make the function works correctly withprotoreflect.Map
  • Write tests to stress as many possibilities as possible

Validate `Enum` values

Now we're not validation the possible Enum values, so if there's a proto file like this:

syntax = "proto3";

message RequestMessage {}

message ResponseMessage {
  enum MyTest {
    ONE = 0;
    TWO = 1;
  }

  int64 responseField = 1;
  MyTest myTest = 2;
}

service MyService {
  rpc MyMethod(RequestMessage) returns (ResponseMessage);
}

And this contract file:

{
  "name": "Some Name Here",
  "services": {
    "MyService": {
      "MyMethod": {
        "successCases": [
          {
            "description": "Should do something",
            "request": {
              "requestField": "VALUE"
            },
            "response": {
              "responseField": 42,
              "myTest": 4
            }
          }
        ]
      }
    }
  }
}

Everything will work but we don't have four Enum options, just two (ONE, TWO) and something like this code below will be generate and probably can lead some errros:

return &ResponseMessage{MyTest: 4, ResponseField: 42}
  • Validates possible Enum values
  • It'll be great to allow the user pass ONE instead of 0

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.