e2e

package module
v0.4.7 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2025 License: MIT Imports: 14 Imported by: 0

README

GitHub tag GitHub code size in bytes

Go-e2e

This is just a small library I mainly wrote over a couple of days one weekend to test my own HTTP APIs. It's only written for my own personal gain. It's not tested and it only supports my own narrow set of requrements. It has since been complemented with a CLI application making usage easier.

Go-e2e was written to be a quick and concurrent facilitator of HTTP API tests.

There are two parts to this projects, a library and a CLI tool, which are located in two separate packages: e2e (module root) and e2r (actually package main). e2e is the library used to define and run test cases while e2r contains the CLI appliction that is used to scan for test cases defined using e2e and initiate an execution.

e2e

e2e is a library letting you define HTTP API tests. Tests are most easily run using the e2r command as explained later. But tests can also be run programmatically by creating and starting a runner. To do this create an empty go-module with a main fuction, create a Runner and call Run on a list of test sets you've declared yourself.

import (
	"github.com/gomsim/go-e2e"
)

func main() {
	e2e.Runner{}.Run(
		AuthSuite,
		EmailSuite,
		NotificationsSuite,
		UsersSuite,

		LoginSequence,
		RegisterUserSequence,
		CreateEventSequence,
	)
}

There is an optional setup and teardown you can provide as functions in the construction of the Runner. This is good if the running of your tests for example need some environment variables set. These functions are typically called BeforeRun and AfterRun

When you run your app you will be presented with a progress bar which when filled will give way to a result summary as well as a prompt giving you the option to see only the logs of failed tests cases or to see the logs of all performed tests (lots of text).

Successful run

But what is a "test"?

Tests

So the whole point of the library is its ability to run test cases. Each Test normally consists of at least a Request and an Expect. The request describes the details of a single HTTP call to be made. The expect describes expectations of the HTTP response. Tests which receive HTTP responses that don't meet the expectations count as failures.

e2e.Test{
    Setup: e2e.Request{
        Method: "GET",
        URL:    "mydomain.com/ping",
    },
    Expect: e2e.Expect{
        Status: 200,
    },
}
Suites

To make testing somewhat feasible and organized tests can be gathered in sets of type Suite. A suite has a name and is a set of independent named tests with no order.

e2e.Suite{
	Name: "myService",
	Tests: e2e.Tests{
		"ping": {
			Request: e2e.Request{
				Method: "GET",
				URL:    "mydomain.com/ping",
			},
			Expect: e2e.Expect{
				Status: 200,
			},
		},
		"create": {
			Request: e2e.Request{
				Method: "POST",
				URL:    "mydomain.com/creatething",
			},
			Expect: e2e.Expect{
				Status: 201,
			},
		},
		"auth": {
			Request: e2e.Request{
				Method: "POST",
				URL:    "mydomain.com/login",
				Body:   `{"user": "username", "password": "password"}`,
			},
			Expect: e2e.Expect{
				Status: 200,
				Headers: e2e.Headers{
					{"Set-Cookie", "session_id=abc123xyz"},
				},
			},
		},
	},
}
Sequences

Some tests require some setup. Or perhaps testing of one HTTP request requires information contained in the response to a different HTTP request. This is where type Sequences comes in. Sequences resemble suites in that they have a name and a collection of tests, but they differ in purpose. A sequence is unsurprisingly sequential meaning tests are run in the order they are declared. Tests within sequences work like steps. This is because tests, or steps, in a sequence are not indipendent but interdependent. They can take input and give output as well as perform pre test actions (tests in suites can also do this, but there is less incentive to do so). A bofore action can be two things, one of which is a manual input func (Input) declared within a step. It is useful when a step requires some external information in order to be performed, such as a pin code or some other information retrieved from a third source. When the tests are run the opportunity will be presented for the user to input the data as needed. The other before action is the ability for the step to run a terminal command (Command), such as a third party program, to for example expose a qr code, or such. Outputs from steps can be caught using a Captor. Captors are declared within a step to let it capture information contained within its HTTP response, such as an oid or URL, and let subsequent steps reference it to perform their own HTTP calls.

e2e.Sequence{
	Name: "finger print - order flow",
	Steps: e2e.Steps{
		{
			Request: e2e.Request{
				Method:  "POST",
				URL:     "mydomain.com/fingerprint/create",
				Content: "application/json",
				Body:    `{"user": "MyUser", "phone": "010111000",}`,
			},
			Expect: e2e.Expect{
				Status: 201,
				Fields: e2e.Fields{
					"message": "OK"
				},
			},
		},
		{
			Before: e2e.Before{
				e2e.Input("finger print", "fingerprint"), // Propmpts the user for "finger print" and stores the input in a memory location called "fingerprint"
			},
			Request: e2e.Request{
				Method:  "POST",
				URL:     "mydomain.com/fingerprint/apply",
				Content: "application/json",
				Body:    `{"print": "$fingerprint"}`, // References the stored "fingerprint"
			},
			Expect: e2e.Expect{
				Status: 200,
				Fields: e2e.Fields{
					"token": "",
				},
			},
			Capture: e2e.Captors{"token"}, // Captures whatever was the value of the "token" field in the response body
		},
		{
			Request: e2e.Request{
				Method:  "POST",
				URL:     "mydomain.com/auth/token",
				Headers: e2e.Headers{{Key: "Authorization", Val: "Bearer $token"}}, // References the stored "token"
			},
			Expect: e2e.Expect{
				Status: 200,
				Fields: e2e.Fields{
					"url": "",
				},
			},
			Capture: e2e.Captors{"url"}, // Captures whatever was the value of the "url" field in the response body
		},
		{
			Request: e2e.Request{
				Method: "POST",
				URL:    "$url", // References the stored "url"
			},
			Expect: e2e.Expect{
				Status: 200,
			},
		},
	},
}

e2r

e2r is a CLI app or CLI command really just eliminating the need to create a separate application to run tests manually. Perhaps it sounds unnecessary, but there is a good reason for it. When defining tests there are two types of changes that can be made between runs, changes related to the domain of what is being tested and changes that only concern what is currently being tested. So an example of the former is a change to the API being tested. Maybe you add a test, or refine a test. These are things you want stored in code, with which e2e provides you the oppertunity. The latter type of change concern things like which ones of all your tests you want to run right now, or within which environment you want to run your tests. Dev? Prod?

The e2r cli command lets you define in code what the tests look like while letting you pass as arguments to the command what tests you want to run and within which environment.

Getting started

To run the e2r command you first need to explicitly install it, even though you have already downloaded the library before.

go install github.com/gombrii/go-e2e/cmd/e2r@latest

You can then run it by standing in your project root, calling it and providing the path or pattern describing whatever packge of tests you want to run. The e2r command works just the same as the go test command in the way it interprets patterns. So if you want to run all tests in your module, simply provide it with ./...

e2r ./...

e2r will look for any exported variable of type Sequence or Suite declared within any of the packages falling within the pattern provided to the cammand. e2r reads these declarations and generates and runs a temporary runnable that references these variables. The remporary runnable will be removed after being run.

AddressBook

There is a second (optional) argument that e2r currently takes, env. This is to enable the possibility for the user to write tests once and run them targeted toward multiple different environments. It is not uncommon to for example first want to run tests against a development environment, then later a pre production environment and a production environment. Eg:

e2r ./... dev

By providing the e2r with a second argument, the value of this argument will be available to the e2e engine at runtime.

e2e uses this value to perform lookups in what's called the AddressBook, which is simply a nested map which you can register to the engine at startup.

func init() {
	e2e.SetAddressBook(e2e.AddressBook{
		"local": {
			"authservice":    "https://localhost:8080/api/v1/auth",
			"userservice":    "https://localhost:8081/api/v1/users",
			"paymentservice": "https://localhost:8082/api/v1/pay",
		},
		"dev": {
			"authservice":    "https://dev.mysite-test.com/api/v1/auth",
			"userservice":    "https://dev.mysite-test.com/api/v1/users",
			"paymentservice": "https://dev.mysite-test.com/api/v1/pay",
		},
		"prod": {
			"authservice":    "https://mysite.com/api/v1/auth",
			"userservice":    "https://mysite.com/api/v1/users",
			"paymentservice": "https://mysite.com/api/v1/pay",
		},
	})
}

Note that the call to SetAddressBook needs to be within your module's init function.

Having done this the addresses of the AddressBook will be available for injection in your tests by calling the Addr function and providing the name of a service. e2e will use that service name in combination with whatever environment was passed to the e2r command to lookup the base address of the service. To append a path simply append it with + or use fmt.Sprint.

	{
		Request: e2e.Request{
			Method: "POST",
			URL:    e2e.Addr("paymentservice") + "/creditcard",
		},
		Expect: e2e.Expect{
			Status: 200,
		},
	},
Setup and teardown

As mentioned under the first example in e2e the test runner can take as arguments a setup function and a teardown function. When running tests using e2r these can exist as well. The difference is that they'll not be provided anywhere. Instead they simply have to be declared and exported in the root package using the signatures func BeforeRun() any and func AfterRun(any) and they will both be automatically run before and after a test session respecively.

Concurrency and performance

From my own manual testing it seems to scale pretty constantly and run whatever amount of tests in about a second, though it's only been tested on at most about 130 tests in one go.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Addr added in v0.4.2

func Addr(svc string) string

func Command

func Command(command string, args ...string) func(data map[string]string) (string, error)

func EnvAddr added in v0.4.3

func EnvAddr(env, svc string) string

func Input

func Input(text string, mapTo string) func(data map[string]string) (string, error)

func SetAddressBook added in v0.4.2

func SetAddressBook(book AddressBook)

Types

type AddressBook added in v0.4.2

type AddressBook map[string]map[string]string

type Before

type Before []func(data map[string]string) (string, error)

type Body

type Body map[string]any

type Captors

type Captors []string

type Expect

type Expect struct {
	Status  int
	Headers Headers
	Body    Body
}

type Headers

type Headers []header

type Request

type Request struct {
	CTX     context.Context
	Method  string
	URL     string
	Headers Headers
	Content string
	Body    string
}

type Runner

type Runner struct {
	BeforeRun func() any
	AfterRun  func(any)
}

func (Runner) Run

func (r Runner) Run(sets ...set)

type Sequence

type Sequence struct {
	Name  string
	Steps Steps
}

type Steps

type Steps []test

type Suite

type Suite struct {
	Name  string
	Tests Tests
}

type Tests

type Tests map[string]test

Directories

Path Synopsis
cmd
e2r command

Jump to

Keyboard shortcuts

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