testing

package
v1.4.7 Latest Latest
Warning

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

Go to latest
Published: Jan 6, 2025 License: Apache-2.0 Imports: 11 Imported by: 0

README

Mock TanzuHub Server for Writing Unit Tests

This package allows plugin authors to easily write unit tests using a mock GraphQL server endpoint for plugins that interact with the Tanzu Hub endpoint.

Note: This feature is EXPERIMENTAL. The API signature and implementation are subject to change or removal if an alternative means of providing equivalent functionality is introduced.

Using the Mock Server to Write Unit Tests

To mock a response for GraphQL queries, plugin authors can follow these simple steps:

  1. Start the mock server:
import hubtesting "github.com/vmware-tanzu/tanzu-plugin-runtime/client/hub/testing"

hubMockServer := hubtesting.NewServer(t, hubtesting.WithQuery(...))
defer hubMockServer.Close()
  1. Create a mockHubClient with the newly started server:
import "github.com/vmware-tanzu/tanzu-plugin-runtime/client/hub"

mockHubClient, err = hub.NewClient(c.CurrentContext, hub.WithEndpoint(hubMockServer.URL), hub.WithAccessToken("fake-token"))
  1. To reuse a server across multiple unit tests, use hubMockServer.Reset() to clean up any previously registered queries, mutations, or errors.

  2. To register mock queries, mutations or subscriptions, use the hubMockServer.RegisterQuery, hubMockServer.RegisterMutation or hubMockServer.RegisterSubscription APIs, which accept Operation objects.

Defining an Operation Object to Mock a Response

Let's assume your GraphQL query looks like the following:

query QueryAllProjects {
  applicationEngineQuery {
    queryProjects(first: 1000) {
      projects {
        json
      }
    }
  }
}

The following Operation type defines what values should be provided to the object:

// Operation is a general type that encompasses the Operation type and Response which
// is of the same type, but with data.
type Operation struct {
    // opType denotes whether the operation is a query, a mutation or a subscription, using the opQuery,
    // opMutation and opSubscription constants. This is unexported as it is set by the *Server.RegisterQuery,
    // *Server.RegisterMutation and *Server.RegisterSubscription functions, respectively.
    opType int

    // Identifier helps identify the operation in a request when coming through the Server.
    // For example, if your operation looks like this:
    //
    //   query {
    //     myOperation(foo: $foo) {
    //       fieldOne
    //       fieldTwo
    //     }
    //   }
    //
    // then this field should be set to myOperation. It can also be more specific, a simple
    // strings.Contains check occurs to match operations. A more specific example of a
    // valid Identifier for the same operation given above would be myOperation(foo: $foo).
    Identifier string

    // Variables represents the map of variables that should be passed along with the
    // operation whenever it is invoked on the Server.
    Variables map[string]interface{}

    // Response represents the response that should be returned whenever the server makes
    // a match on Operation.opType, Operation.Identifier, and Operation.Variables.
    // Note: This is to be used for Query and Mutation operations only.
    Response interface{}

    // EventGenerator should generate mock events for Subscription operation type
    // Note: This is only to be used for the Subscription where you will need to
    // mock the generation of the events. This should not be used with Query or Mutation.
    EventGenerator EventGenerator
}

If you have already generated the Go stubs using make tanzu-hub-stub-generate, all the Go type definitions necessary for writing the sample response should be available to you. These definitions might look like this based on the defined schema:

// QueryAllProjectsApplicationEngineQuery includes the requested fields of the GraphQL type ApplicationEngineQuery.
type QueryAllProjectsApplicationEngineQuery struct {
    QueryProjects QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnection `json:"queryProjects"`
}

// QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnection includes the requested fields of the GraphQL type KubernetesKindProjectConnection.
type QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnection struct {
    Projects []QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnectionProjectsKubernetesKindProject `json:"projects"`
}

// QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnectionProjectsKubernetesKindProject includes the requested fields of the GraphQL type KubernetesKindProject.
type QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnectionProjectsKubernetesKindProject struct {
    // Raw JSON response as produced by KRM API
    Json json.RawMessage `json:"json"`
}

To create a testing.Operation object, we will reuse these generated type definitions to create a mock response object:

import hubtesting "github.com/vmware-tanzu/tanzu-plugin-runtime/client/hub/testing"

type QueryResponse struct {
    ApplicationEngineQuery hub.QueryAllProjectsApplicationEngineQuery
}

mockResponse := hubtesting.Operation{
    Identifier: "QueryAllProjects",
    Response: QueryResponse{
        hub.QueryAllProjectsApplicationEngineQuery{
            QueryProjects: hub.QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnection{
                Projects: []hub.QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnectionProjectsKubernetesKindProject{
                    {
                        Json: jsonBytesForProject1,
                    },
                    {
                        Json: jsonBytesForProject2,
                    },
                },
            },
        },
    },
}

hubMockServer.RegisterQuery(mockResponse)

Documentation

Overview

Package testing exports a GraphQL Mock Server that facilitates the testing of client.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type EventGenerator

type EventGenerator func(ctx context.Context, receivedRequest Request, eventData chan<- Response)

EventGenerator should implement a eventData generator for testing and send mock event response to the `eventData` channel. To suggest the end of the event responses from the server side, you can close the eventData channel

type Operation

type Operation struct {

	// Identifier helps identify the operation in a request when coming through the Server.
	// For example, if your operation looks like this:
	//
	//	query {
	//		myOperation(foo: $foo) {
	//			fieldOne
	//			fieldTwo
	//		}
	//	}
	//
	// then this field should be set to myOperation. It can also be more specific, a simple
	// strings.Contains check occurs to match operations. A more specific example of a
	// valid Identifier for the same operation given above would be myOperation(foo: $foo).
	Identifier string

	// Variables represents the map of variables that should be passed along with the
	// operation whenever it is invoked on the Server.
	Variables map[string]interface{}

	// Response represents the response that should be returned whenever the server makes
	// a match on Operation.opType, Operation.Identifier, and Operation.Variables keys.
	// Response is to be used for Query and Mutation operations only.
	// Note: User can define either `Response` or implement `Responder` function but should
	// not be defining both.
	Response hub.Response

	// Responder implements the function that based on some operation parameters should respond
	// differently.
	// Tests that do not need flexibility in returning different responses based on the received
	// request should just configure the `Response` field instead.
	// Responder is to be used for Query and Mutation operations only.
	// Note: User can define either `Response` or implement `Responder` function but should
	// not be defining both.
	Responder Responder

	// EventGenerator should implement a eventData generator for testing and
	// send mock event response to the `eventData` channel. To suggest the end of
	// the event responses from server side, you can close the eventData channel
	// Note: This is only to be used for the Subscription where you will need to
	// mock the generation of the events. This should not be used with Query or Mutation.
	EventGenerator EventGenerator
	// contains filtered or unexported fields
}

Operation is a general type that encompasses the Operation type and Response which is of the same type, but with data.

type OperationError

type OperationError struct {
	// Identifier helps identify the operation error in a request when coming through the Server.
	// For example, if your operation looks like this:
	//
	//	error {
	//		myOperation(foo: $foo) {
	//			fieldOne
	//			fieldTwo
	//		}
	//	}
	//
	// Then this field should be set to myOperation. It can also be more specific, a simple
	// strings.Contains check occurs to match operations. A more specific example of a
	// valid Identifier for the same operation given above would be myOperation(foo: $foo).
	Identifier string

	// Status represents the http status code that should be returned in the response
	// whenever the server makes a match on OperationError.Identifier
	Status int

	// Error represents the error that should be returned in the response whenever
	// the server makes a match on OperationError.Identifier
	Error error

	// Extensions represents the object that should be returned in the response
	// as part of the api error whenever the server makes a match on OperationError.Extensions
	Extensions interface{}
}

OperationError is a special type that brings together the properties that a response error can include.

type Request

type Request struct {
	Query     string                 `json:"query"`
	Variables map[string]interface{} `json:"variables"`
}

type Responder added in v1.4.2

type Responder func(ctx context.Context, receivedRequest Request) hub.Response

Responder implements the function that based on some operation parameters should respond differently. This type of Responder implementation is more useful when you want to implement a generic function that returns data based on the received Request

type Response

type Response struct {
	Data   interface{}     `json:"data"`
	Errors []ResponseError `json:"errors,omitempty"`
}

type ResponseError

type ResponseError struct {
	Message    string      `json:"message"`
	Path       []string    `json:"path"`
	Extensions interface{} `json:"extensions"`
}

type Server

type Server struct {
	URL string
	// contains filtered or unexported fields
}

func NewServer

func NewServer(t *testing.T, opts ...ServerOptions) *Server

NewServer returns a Mock Server object. The server object returned contains a closing function which should be immediately registered using t.Cleanup after calling NewServer, example:

ts := testing.NewServer(t)
t.Cleanup(ts.Close)

If you want to reuse a server across multiple unit tests than use ts.Reset() to clean up any already registered queries, mutations or errors

func (*Server) Close

func (s *Server) Close()

Close closes the underlying httptest.Server.

func (*Server) Do

func (s *Server) Do(r Request) Response

Do takes a Request, performs it using the underlying httptest.Server, and returns a Response.

func (*Server) Mutations

func (s *Server) Mutations() []Operation

Mutations returns the registered mutations that the server will accept and respond to.

func (*Server) Queries

func (s *Server) Queries() []Operation

Queries returns the registered queries that the server will accept and respond to.

func (*Server) RegisterError

func (s *Server) RegisterError(operation OperationError)

RegisterError registers an OperationError as an error that the server will recognize and respond to.

func (*Server) RegisterMutation

func (s *Server) RegisterMutation(operations ...Operation)

RegisterMutation registers an Operation as a mutation that the server will recognize and respond to.

func (*Server) RegisterQuery

func (s *Server) RegisterQuery(operations ...Operation)

RegisterQuery registers an Operation as a query that the server will recognize and respond to.

func (*Server) RegisterSubscription

func (s *Server) RegisterSubscription(operations ...Operation)

RegisterSubscription registers an Operation as a subscription that the server will recognize and respond to.

func (*Server) Reset

func (s *Server) Reset()

Reset resets the existing mocked responses that are already registered with the server

func (*Server) Subscriptions

func (s *Server) Subscriptions() []Operation

Subscriptions returns the registered subscriptions that the server will accept and respond to.

type ServerOptions

type ServerOptions func(o *Server)

func WithErrors

func WithErrors(operations []OperationError) ServerOptions

WithErrors registers mock OperationError to the server

func WithMutation

func WithMutation(operations ...Operation) ServerOptions

WithMutation registers mock Mutation operations to the server

func WithQuery

func WithQuery(operations ...Operation) ServerOptions

WithQuery registers mock Query operations to the server

func WithSubscriptions

func WithSubscriptions(operations ...Operation) ServerOptions

WithSubscriptions registers mock Subscriptions operations to the server

Jump to

Keyboard shortcuts

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