fakes

package module
v1.1.4 Latest Latest
Warning

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

Go to latest
Published: Apr 26, 2025 License: MIT Imports: 10 Imported by: 0

README

Fakes

More complex applications tend to require communicating with a plethora of different services. It's not always possible to write tests that exercise the APIs you depend upon, but this lib is designed to make defining fake test fixtures easier and more ergonomic for developers.

This lib removes the complexity of lifecycle management around the httptest server and provides some niceties that make fleshing out fakes far more straightforward. This means your acceptance/integration tests aren't awash with various HTTP setup code and you can focus on what really matters.

This is ideal if you need to fake out multiple downstream dependencies whilst keeping your acceptance tests readable and succinct.

Getting Started

This lib is somewhat opinionated such that writing happy path test cases can be done in as few lines as possible.

import "github.com/elliotforbes/fakes"

You can then start defining in-memory fakes that have lifecycles as long as your test functions like so:

downstreamAPI := fakes.New()
downstreamAPI.Endpoint(&fakes.Endpoint{
    Path: "/some/path/my/app/hits",
    Response: `{"status": "great success"}`,
})
downstreamAPI.Run(t)

Hitting this API will result in the following response:

HTTP GET /some/path/my/app/hits 

// returns a response:

HTTP 200 
Content-Type: application/json

{"status": "great success"}

Specifying A Port

There are some instances where you need to specify a port upon which these fakes will run. To achieve this, you can pass in the fakes.WithPort(N) optional parameter:

downstreamAPI := fakes.New(fakes.WithPort(10000))
...
Endpoints

Endpoints are a core concept for this lib. You can define 1 or more Endpoints for your fake service and you have full control of what happens in the event of this endpoint being called:

downstreamAPI.Endpoint(&fakes.Endpoint{
    Path: "/some/path/my/app/hits",
    Response: `{"status": "great success"}`,
    // if ContentType is not specified, we assume `application/json`
    ContentType: "plaintext",
    // if statuscode is not specified, we assume a 200
    // status code response
    StatusCode: http.StatusUnauthorised,
    // if Methods is not specified, we assume it could
    // hit any HTTP method
    Methods: []string{
        http.MethodGet,
    },
    Headers: fakes.Headers{
		"Authorization": "Bearer some-bearer",
    },
    Expectation: func(r *http.Request) {
        // run assertions on the incoming http request to ensure
        // that you are sending the right data to any of the APIs
        // that you depend upon.
    },
})

Note: if you define an endpoint for your test, it will need to be hit at least once in order for the tests to pass. This is to help prevent unused endpoints from being defined within your tests.

Stringing Your Setup Together

The fakes library includes the ability to string your setup together in one succinct assertion. Let's take a look at an example of setting this up with 2 distinct endpoints:

fakeServer := fakes.New().
    Endpoint(&fakes.Endpoint{
        Path:     "/",
        Response: "{}",
    }).
    Endpoint(&fakes.Endpoint{
        Path:     "/hello",
        Response: `{"message":"hello"}`,
    }).Run(t)

A nice bit of syntactic sugar for those that prefer this approach!

Chaos Engineering

In your acceptance tests, it can be useful to model some level of unreliability within your tests so that you are consistently handling retries and have a good level of resiliency baked into your system.

This can invariably lead to flaky tests depending on how unlucky you are. You might have clients that have backoff retries and a retry limit of 5, but somehow, you roll 1's 5 times and all of these requests fail.

I might eventually need a higher level of determinism within my acceptance tests, if that time comes, I can start to consider how I can expose controls that will mean that subsequent requests pass.

If you'd like to include an element of chaos into your tests, you can specify the FailureRatePercent field on each of your endpoints and it'll fail depending on that provided percentage:

fakeServer := fakes.New().
    Endpoint(&fakes.Endpoint{
        Path:               "/",
        Response:           "{}",
        // 100 means this will always fail
        FailureRatePercent: 100,
        // We can specify how we'd like the failure
        // to respond
        FailureHandler: func(c *gin.Context) {
            c.JSON(http.StatusBadRequest, gin.H{
                "error": "something bad happened",
            })
        },
    }).Run(t)

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func WithPort added in v1.1.1

func WithPort(port int) func(*FakeService)

Types

type Endpoint

type Endpoint struct {
	Path        string
	Response    string
	StatusCode  int
	Methods     []string
	ContentType string
	Headers     Headers
	Handler     func(*gin.Context)
	// FailureRatePercent - allows you to specify the probability
	// of failure for your Endpoint. I.e. 0.8 represents and 80%
	// chance you'll be met with a 500 response.
	FailureRatePercent int
	// FailureHandler - allows you to define a custom failure
	// handler so that you can model how your upstream dependencies
	// could fail.
	FailureHandler func(*gin.Context)

	// Expectation - it can be handy to specify assertions
	// in the context of the tests you are developing. This
	// will allow you to make assertions on the request that
	// eventually makes it to your fake.
	Expectation func(*http.Request)
	// contains filtered or unexported fields
}

Endpoint - represents an Endpoint defined under the context of a FakeService.

type FakeService

type FakeService struct {
	Endpoints []*Endpoint

	Port    int
	BaseURL string
	// contains filtered or unexported fields
}

FakeService - the core of this lib. features a ref to a gin.Engine and a slice of Endpoints. BaseURL is an exposed field which is set at the point at which the Run method is called.

func New added in v1.1.0

func New(opts ...func(*FakeService)) *FakeService

NewFakeHTTP - a constructor that spins up a new reference to a FakeService.

func (*FakeService) Endpoint added in v1.1.0

func (f *FakeService) Endpoint(e *Endpoint) *FakeService

Endpoint - registers a new endpoint on the fake service. This will set some sensible defaults should the Endpoint not have them explicitly defined. For example, we default to HTTP 200 statuses and a Content-Type of 'application/json'. Whenever said endpoint is called, we ensure that we record the call and increment the `calls` field.

func (*FakeService) Run

func (f *FakeService) Run(t *testing.T) *FakeService

Run - starts up the fake service. This creates a custom net listener which then replaces the testserver listener. This was due to communication issues between docker containers originally, however, this argument may no longer hold water.

func (*FakeService) TidyUp

func (f *FakeService) TidyUp(t *testing.T)

TidyUp - this method ranges over all of the endpoints defined under this FakeService and ensures that each of them have been called at least once. If the call count is 0, then this will fail the test that depends on this fake service.

type Headers added in v1.0.5

type Headers map[string]string

Jump to

Keyboard shortcuts

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