echoprobe

package module
v1.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2025 License: Apache-2.0 Imports: 26 Imported by: 0

README

echoprobe 🧪

echoprobe is a simple Go library for writing integration tests in for echo framework. It uses test-containers and gock to provide a simple and easy-to-use interface to write tests. supporting real HTTP requests and responses in various formats.

Features

  • Real HTTP requests and responses in JSON and Excel formats
  • Mocking of external HTTP calls
  • Database integrations with PostgreSQL and BigQuery
  • Fully compatible with fastecho

Get Started

Install echoprobe, using go get:

$ go get github.com/ingka-group/echoprobe

Below you can find a list of examples in order to use echoprobe.

You can find some complete examples in the test directory.

Basic usage

To write an integration test, you need to create a new test file with the _test.go suffix. In case your test cases of your handlers expect a response, you can leverage the fixtures feature. Fixtures are optional, and are required only in case you expect a response body. A fixtures folder has to exist in the location where the _test.go files are so that files can be read. This also ensures that fixtures are kept close to the test and in the relevant package. To store the expected responses, you need to create a responses directory under fixtures to store your JSON responses. For example, fixtures/responses/my_response.json.

it := echoprobe.NewIntegrationTest(
    t,
)
defer func() {
    it.TearDown()
}()

handler := NewHandler()

tests := []echoprobe.Data{
    {
        Name:   "ok: my test case",
        Method: http.MethodGet,
        Params: echoprobe.Params {
            Path: map[string]string {
                "id": "1",
            },
        },
        Handler:        handler.MyEndpoint,
        ExpectCode:     http.StatusOK,
        ExpectResponse: "my_response",
    },
}

echoprobe.AssertAll(it, tests)
With PostgreSQL

To use PostgreSQL in your integration test, you need to pass the IntegrationTestWithPostgres option to the NewIntegrationTest function. Optionally, you can initialize your database using a SQL script, that will be executed before the test starts. The script should contain the necessary DDL and DML statements to prepare the database for the test. The script must be present under fixtures. For example, fixtures/init-db.sql.

it := echoprobe.NewIntegrationTest(
    t,
    echoprobe.IntegrationTestWithPostgres{
        InitSQLScript: "init-db.sql",
    },
)

defer func() {
    it.TearDown()
}()

repository := NewRepository(it.Db)
service :=    NewService(repository)
handler :=    NewHandler(service)

tests := []echoprobe.Data{...}

echoprobe.AssertAll(it, tests)
With BigQuery

echoprobe supports testing with BigQuery using ghcr.io/goccy/bigquery-emulator as a test contair. To use BigQuery in your integration test, you need to pass the IntegrationTestWithBigQuery option to the NewIntegrationTest function. It is expected that BigQuery needs to be populated with data upon the test startup. To do that, you need to provide a .yaml under the fixtures/bigquery directory. The YAML file should contain the necessary format so that BigQuery emulator can mount the data in the container.


it := echoprobe.NewIntegrationTest(
    t,
    echoprobe.IntegrationTestWithBigQuery{
        DataPath: "/fixtures/bigquery/data.yaml",
    },
)

bqClient, err := NewBigQueryClient(it.BigQuery)

repository := NewRepository(bqClient)
service :=    NewService(repository)
handler:=     NewHandler(service)

tests := []echoprobe.Data{...}

echoprobe.AssertAll(it, tests)

func NewBigQueryClient(t *testing.T, bq *echoprobe.BigqueryEmulatorContainer) (*bigquery.Client, error) {
    client, err := bigquery.NewClient(
        context.Background(),
        "test",
        option.WithoutAuthentication(),
        option.WithEndpoint(fmt.Sprintf("http://%s:%d", bq.BqHost, bq.BqRestPort)),
    )

    defer func(client *bigquery.Client) {
        err := client.Close()
        if err != nil {
            t.Fatalf("unable to close big query client: %v", err)
        }
    }(client)

    return client, err
}
With Mocks

Mock responses are optional and must be stored with the rest of the fixtures as .json files in a mocks folder within fixtures. For example, a mock called my_mock.json would be stored in fixtures/mocks/my_mock.json. Mocking a request, consists of pairing a request URL with a status code and optionally a response.

NOTE: Mocks are single-use. If your code repeatedly calls the same endpoint expecting the same response, writing this once is not enough. You need to replicate the mock itself to make a request reuse a mock a specific number of times.

it := echoprobe.NewIntegrationTest(
    t,
    echoprobe.IntegrationTestWithMocks{
        BaseURL: "/v1",
    },
)
defer func() {
    it.TearDown()
}()

handler := NewHandler()

tests := []echoprobe.Data{
    {
        Name:   "ok: my test case",
        Method: http.MethodGet,
        Params: echoprobe.Params {
            Path: map[string]string {
                "id": "1",
            },
        },
        Mocks: []echoprobe.MockCall{
            {
                Config: &echoprobe.MockConfig{
                    UrlPath:    fmt.Sprintf("/v1/users/%s", "1"),
                    Response:   "my_mock",
                    StatusCode: http.StatusOK,
                },
            },
        },
        Handler:        handler.MyEndpoint,
        ExpectCode:     http.StatusOK,
        ExpectResponse: "my_response",
    },
}

echoprobe.AssertAll(it, tests)
Intercepting a custom http.Client

By default, echoprobe only listens to the baseUrl on the http.DefaultClient. When using a custom http.Client for HTTP calls, echoprobe should be notified. The custom client can be added after initializing the integration tests:

it := echoprobe.NewIntegrationTest(
    t,
    echoprobe.IntegrationTestWithMocks{
        BaseURL: "/v1",
    },
)

client := http.Client{Transport: &http.Transport{}}
it.Mock.SetHttpClient(&client)
With PostgreSQL and Mocks

An integration test support various types of features all at once. In order to use PostgreSQL and Mocks, you can use the following example. Similarly, you can append other options to your integration test.

it := echoprobe.NewIntegrationTest(
    t,
    echoprobe.IntegrationTestWithMocks{
        BaseURL: "/v1",
    },
    echoprobe.IntegrationTestWithPostgres{},
)
With Excel

echoprobe supports testing with Excel files. To compare the result of a handler with the expected Excel file, you need to store the Excel file(s) under excel in the fixtures folder. For example, fixtures/excel/my_excel.xlsx. Additionally, you need to pass some extra instructions to the test case, to identify that the response is expected to be an Excel file.

tests := []echoprobe.Data{
    {
        Name:   "ok: my test case",
        Method: http.MethodGet,
        Params: echoprobe.Params {
            Path: map[string]string {
                "id": "1",
            },
        },
        Handler:            handler.MyEndpoint,
        ExpectCode:         http.StatusOK,
        ExpectResponseType: echoprobe.Excel,
        ExpectResponse:     "my_excel",
    },
}
Error responses

echoprobe is fully compatible with the echo.NewHTTPError response, meaning that you can test error responses as well by setting the ExpectErrResponse field to true in the test data.

tests := []echoprobe.Data{
    {
        Name:   "error: my test case",
        Method: http.MethodGet,
        Params: echoprobe.Params {
            Path: map[string]string {
                "id": "INVALID_ID",
            },
        },
        Handler:           handler.MyEndpoint,
        ExpectCode:        http.StatusBadRequest,
        ExpectErrResponse: true,
    },
}
Query parameters

echoprobe supports also query parameters in the request. You can pass them in the Params of the Data struct. The structure supports multiple query parameters with the same key to cover situations where the endpoint supports multiple values for the same parameter.

tests := []echoprobe.Data{
    {
        Name:   "ok: my test case",
        Method: http.MethodGet,
        Params: echoprobe.Params {
            Body: "my_body",
            Query: map[string][]string {
                "param1": {"value1", "value2"},
            }
        },
        Handler:    handler.MyEndpoint,
        ExpectCode: http.StatusNoContent,
    },
}
Request body

In case your request required a body, you can pass it in the Data struct. In such cases, you need to store the JSON of the request body under requests in the fixtures folder. For example, fixtures/requests/my_body.json.

tests := []echoprobe.Data{
    {
        Name:   "ok: my test case",
        Method: http.MethodPost,
        Params: echoprobe.Params {
            Body: "my_body",
        },
        Handler:        handler.MyEndpoint,
        ExpectResponse: "my_response",
        ExpectCode:     http.StatusCreated,
    },
}
Assert with custom context

echoprobe provides the function AssertAll to assert the test cases. In case you need to assert the test cases with a custom context, you can create your own function for that.

// AssertAllWithCustomContext is a helper function to run multiple tests in a single test function.
// Before asserting a test, the function prepares the custom context and calls the handler function.
func AssertAllWithCustomContext(it *echoprobe.IntegrationTest, tt []echoprobe.Data) {
    for _, t := range tt {
        ctx, response := echoprobe.Request(it, t.Method, t.Params)

        // Create the custom context
        sctx := &CustomContext{
            Context:   ctx,
            Clock:     clock.NewMock(),
        }

        // Attach the custom context to the handler
        err := t.Handler(sctx)
        if err != nil {
            it.T.Log(err.Error())
        }

        // Assert the test
        echoprobe.Assert(it, &t, &echoprobe.HandlerResult{
            Err:      err,
            Response: response,
        })
    }
}
Testing

To run the full set of tests you can execute the following command.

$ make test
Pre-commit

This project uses pre-commit(https://pre-commit.com/) to integrate code checks used to gate commits.

# required only once
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit

# run checks on all files
$ make pre-commit
Other management tasks

For more information on the available targets, you can access the inline help of the Makefile.

$ make help

or, equivalently,

$ make

Contributing

Please read CONTRIBUTING for more details about making a contribution to this open source project and ensure that you follow our CODE_OF_CONDUCT.

Contact

If you have any other issues or questions regarding this project, feel free to contact one of the code owners/maintainers for a more in-depth discussion.

Licence

This open source project is licensed under the "Apache-2.0", read the LICENCE terms for more details.

Documentation

Index

Constants

View Source
const (
	Excel = "xlsx"
	CSV   = "csv"
)

Variables

This section is empty.

Functions

func Assert

func Assert(it *IntegrationTest, t *Data, res *HandlerResult)

Assert asserts the result of a handler. Leveraging the HandlerResult struct allows us to assert the response of a request by calling the handler function outside the assertion method. Thus, we can pass a custom context when calling the handler.

Example:

	for _, t := range tt {
	  ctx, response := Request(it, t.Method, t.Params)

	  sctx := &CustomContext{
		Context:   ctx,
		Clock:     clock.NewMock(),
	  }

	  err := t.Handler(sctx)
   	  if err != nil {
		it.T.Log(err.Error())
	  }

	  t.Assert(it, &HandlerResult{
		err:      err,
		response: response,
	  })
	}

func AssertAll

func AssertAll(it *IntegrationTest, tt []Data)

AssertAll runs the given tests and asserts their result. The handler function is called inside the assertion method.

func LoadMocks

func LoadMocks(it *IntegrationTest, t *Data)

LoadMocks loads the mocks for a given test case.

func Request

func Request(it *IntegrationTest, method string, params Params) (echo.Context, *httptest.ResponseRecorder)

Request creates a new request and a new test service context to which it passes the required parameters.

Types

type BigqueryEmulatorContainer

type BigqueryEmulatorContainer struct {
	testcontainers.Container

	BqHost     string
	BqRestPort int
	BqGrpcPort int
}

type Data

type Data struct {
	Name               string
	Method             string
	Params             Params
	Handler            func(ctx echo.Context) error
	Mocks              []MockCall
	ExpectResponse     string
	ExpectErrResponse  bool
	ExpectCode         int
	ExpectResponseType string
}

Data is a helper struct to define the parameters of a request for a test case.

type Fixtures

type Fixtures struct{}

Fixtures is a helper for reading fixtures.

func (Fixtures) ExcelToMap

func (f Fixtures) ExcelToMap(content []byte) (map[string][][]string, error)

func (Fixtures) ReadCsvFile added in v1.0.0

func (f Fixtures) ReadCsvFile(s string) string

ReadExcelFile reads an excel file with xlsx extension.

func (Fixtures) ReadExcelFile

func (f Fixtures) ReadExcelFile(s string) map[string][][]string

ReadExcelFile reads an excel file with xlsx extension.

func (Fixtures) ReadFixture

func (f Fixtures) ReadFixture(filename, dir string) string

ReadFixture reads a fixture from a file.

func (Fixtures) ReadRequestBody

func (f Fixtures) ReadRequestBody(s string) string

ReadRequestBody reads the request body from a file.

func (Fixtures) ReadResponse

func (f Fixtures) ReadResponse(s string) string

ReadResponse reads the response from a file.

type HandlerResult

type HandlerResult struct {
	Err      error
	Response *httptest.ResponseRecorder
}

HandlerResult holds the result of a handler, the error that possibly was returned and the response recorder.

type IntegrationTest

type IntegrationTest struct {
	T           *testing.T
	Db          interface{}
	Echo        *echo.Echo
	Fixtures    *Fixtures
	Container   *PostgresDBContainer
	BqContainer *BigqueryEmulatorContainer
	Mock        *Mock
	// contains filtered or unexported fields
}

IntegrationTest is a struct that holds all the necessary information for integration testing.

func NewIntegrationTest

func NewIntegrationTest(t *testing.T, opts ...IntegrationTestOption) *IntegrationTest

NewIntegrationTest prepares database for integration testing.

func (*IntegrationTest) TearDown

func (it *IntegrationTest) TearDown()

TearDown cleans up the database after integration testing.

type IntegrationTestOption

type IntegrationTestOption interface {
	// contains filtered or unexported methods
}

IntegrationTestOption is an interface for integration test options.

type IntegrationTestWithBigQuery

type IntegrationTestWithBigQuery struct {
	DataPath string
}

IntegrationTestWithBigQuery is an option for integration testing that sets up a BigQuery database test container.

type IntegrationTestWithMocks

type IntegrationTestWithMocks struct {
	BaseURL string
}

IntegrationTestWithMocks is an option for integration testing that allows mocking The mocks should be placed in a 'mocks' directory where the _test.go file is located.

type IntegrationTestWithPostgres

type IntegrationTestWithPostgres struct {
	InitSQLScript string
	Config        *gorm.Config
}

IntegrationTestWithPostgres is an option for integration testing that sets up a postgres database test container. In the InitSQLScript a SQL script filename can be passed to initialize the database. The script should be located under a 'fixtures' directory where the _test.go file is located. An optional gorm config can also be passed.

type Mock

type Mock struct {
	// contains filtered or unexported fields
}

Mock is the struct that gives access to all the mocks

func NewMock

func NewMock(baseURL string) *Mock

NewMock creates a new Mock

func (*Mock) Debug

func (m *Mock) Debug()

Debug is used to print the request URL and the mock returned for that particular request

func (*Mock) MockRequest

func (m *Mock) MockRequest(config *MockConfig)

func (*Mock) SetHttpClient added in v1.1.0

func (m *Mock) SetHttpClient(httpClient *http.Client)

func (*Mock) SetJSON

func (m *Mock) SetJSON(response *gock.Response, config *MockConfig)

func (*Mock) TearDown

func (m *Mock) TearDown()

TearDown removes all the registered mocks

type MockCall

type MockCall struct {
	Function func(config *MockConfig)
	Config   *MockConfig
}

MockCall represents a mocked API call used in tests

type MockConfig

type MockConfig struct {
	Method     string
	StatusCode int
	UrlPath    string
	Response   string
}

MockConfig configures a Mock

type Params

type Params struct {
	Path  map[string]string
	Query map[string][]string
	Body  string
}

Params define the parameters of a request.

type PostgresDBContainer

type PostgresDBContainer struct {
	testcontainers.Container

	DBHost     string
	DBPort     int
	DBName     string
	DBUsername string
	DBPassword string
}

PostgresDBContainer holds all the necessary information for postgres database test container.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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