Skip to main content

Write integration tests

In this section we will be looking at several types of integration tests provided by Gosoline. All of them can be used together in the same test, and Gosoline's suite package is responsible for reading their configuration, starting and running an application, then running the actual tests cases. Below is their description:

Base test case

This is the simplest type of Gosoline test case. All it needs is a suite object having a method that starts with Test, which has no inputs nor outputs. An example can be found in more_details/stream-consumer:

func (s *ConsumerTestSuite) TestComponents() {
s3 := s.Env().Component("s3", "default")
s.NotNil(s3)

streamInput := s.Env().Component("streamInput", "consumerInput")
s.NotNil(streamInput)

streamOutput := s.Env().Component("streamOutput", "publisher-outputEvent")
s.NotNil(streamOutput)
}

This particular test makes use of suite.Suite's methods to get and check if three components have been wired.

Application test case

Application test cases are just like Base test cases, the only difference being that they make use of an suite.AppUnderTest object. An example can be found in the same file:

func (s *ConsumerTestSuite) TestSuccessTwice(app suite.AppUnderTest) {
consumer := s.Env().StreamInput("consumerInput")
s.NotNil(consumer)

consumer.Publish(mdl.Box(uint(2)), nil)
consumer.Publish(mdl.Box(uint(3)), nil)

app.Stop()
app.WaitDone()

var result int
s.Env().StreamOutput("publisher-outputEvent").Unmarshal(0, &result)
s.Equal(3, result)

s.Env().StreamOutput("publisher-outputEvent").Unmarshal(1, &result)
s.Equal(4, result)
}

This test publishes two items into the application's stream input, waits until the application is done, then reads its outputs, and compares them with their expected values.

Httpserver test case

If you want to make a standard API call and read the response, you can use Gosoline's HttpserverTestCase. This type of test issues HTTP calls and compares its responses against predefined values and structures. Here's an example:

server_test.go
func (s *HttpTestSuite) Test_Euro() *suite.HttpserverTestCase {
return &suite.HttpserverTestCase{
Method: http.MethodGet,
Url: "/euro/10/GBP",
Headers: map[string]string{},
ExpectedStatusCode: http.StatusOK,
Assert: func(response *resty.Response) error {
result, err := strconv.ParseFloat(string(response.Body()), 64)
s.NoError(err)
s.Equal(8.0, result)

return nil
},
}
}

Notice the predefined structure of an HttpserverTestCase: it is an object that has fields for all the information needed in performing an HTTP call, the expected status code, and a method that tests the result for correctness.

In the example from our HTTP server test tutorial we see two types of test cases (Httpserver and Httpserver extended) in the same test suite. In fact, all types of tests can be part of the same test suite.

Httpserver extended test case

In the Test your HTTP server example, we saw the following test:

server_test.go
func (s *HttpTestSuite) Test_ToEuro(_ suite.AppUnderTest, client *resty.Client) error {
var result float64

// Make a GET request to /euro/:amount/:currency where :amount = 10 and :currency = GBP
response, err := client.R().
SetResult(&result).
Execute(http.MethodGet, "/euro/10/GBP")

// Check that there is no error.
s.NoError(err)

// Check that the response status code is 200 OK.
s.Equal(http.StatusOK, response.StatusCode())

// Check that the converted amount is 8.0.
s.Equal(8.0, result)

return nil
}

The Test_ToEuro method makes a GET call to an endpoint, in order to receive back the euro exchange value for 10 GBP. Lastly, it checks if the received value is 8.0.

In order to make this GET call Test_ToEuro does not need to concern itself about the IP on which the exchange application is running, nor its port. All Test_ToEuro needs to know is the URL path of that endpoint, as the client object does the rest.

This client object is provided by Gosoline, whenever at least one of a test suite's methods has the above signature. The main advantage of the client object is that it allows you to control when requests are executed, and gives you access to the endpoint, by providing the host and port. Therefore, for example, if you want to time your requests, this is the way to go.

server_test.go
func (s *HttpTestSuite) SetupApiDefinitions() httpserver.Definer {
return ApiDefiner
}

Implementing SetupApiDefinitions when configuring your test will inform Gosoline that an API is being tested, thus the client object will contain meaningful data.

Forgetting to implement SetupApiDefinitions, and the trying to run an Httpserver, or an Httpserver extended test case, will result in an error reminding you that the suite has to implement the TestingSuiteApiDefinitionsAware interface.

Stream test case

If you have a Gosoline application that takes its input from a stream, you need a test which can run your application locally, send data to a stream, and check your application's output for correctness.

The stream-consumer application, found in more_details/stream-consumer, reads unsigned integers from the consumerInput input, increments them by one, and publishes them to the publisher-outputEvent output. Below is an extract from its integration test:

func (s *ConsumerTestSuite) SetupSuite() []suite.Option {
return []suite.Option{
suite.WithLogLevel("debug"),
suite.WithConfigFile("../stream-consumer/config.dist.yml"),
suite.WithModule("consumerModule", stream.NewConsumer("uintConsumer", consumer.NewConsumer())),
}
}

It is making use of the same config.dist.yml file as stream-consumer, and it will be using the module created by consumer.NewConsumer. A StreamTestCase is very similar to an HttpserverTestCase:

func (s *ConsumerTestSuite) TestSuccess() *suite.StreamTestCase {
return &suite.StreamTestCase{
Input: map[string][]suite.StreamTestCaseInput{
"consumerInput": {
{
Attributes: nil,
Body: mdl.Box(uint(5)),
},
},
},
Assert: func() error {
var result int
s.Env().StreamOutput("publisher-outputEvent").Unmarshal(0, &result)

s.Equal(6, result)

return nil
},
}
}

This StreamTestCase is an object defining an input and an Assert function. Gosoline will run the module, use this StreamTestCaseInput as input for it, then run Assert. The Assert function reads the first element from a stream, publisher-outputEvent, then compares it with an expected result. Notice that the input has a key called consumerInput, because in this application's config.dist.yml file, we have configured an input named consumerInput.

Notice how the stream output object was obtained: s.Env().StreamOutput("publisher-outputEvent"). In a similar manner, Gosoline's suite.Suite object can provide other useful components: s.Env().DynamoDb("default").Client(), s.Env().MySql("default")., s.Env().Redis("default").Client(), etc.

Subscriber test case

The subscriber test case is similar to a StreamTestCase. It needs a methods whose name starts with Test, has a suite.Suite receiver, and returns an suite.SubscriberTestCase and an error.

func (s *SubscriberTestSuite) TestSuccess() (suite.SubscriberTestCase, error) {
return suite.DdbTestCase(suite.DdbSubscriberTestCase{
Name: "client",
SourceModelId: "mcoins.marketing.management.client",
TargetModelId: "mcoins.marketing.terminal-affiliate-click.client",
Input: &terminal_affiliate_click.ClientInputV0{
Id: 42,
StoreId: "my.store.id",
},
Assert: func(t *testing.T, fetcher *suite.DdbSubscriberFetcher) {
actual := &terminal_affiliate_click.Client{}
fetcher.ByHash(uint(42), actual)

expected := &terminal_affiliate_click.Client{
Id: 42,
StoreId: "my.store.id",
}

s.Equal(expected, actual)
},
})
}

This test will publish an item to an input, stops the application and waits for it to finish, then looks inside a ddd table to see if it was written there.

External Dependencies

The test suite supports configuring and launching external dependencies as docker containers via its environment component. An overview over supported can be found inside the test/env module.

Auto detect components

Another option each suite test offers is WithoutAutoDetectedComponents. This simply adds one extra options to the test, which tells it to skip one of the components configured in any potential config.dist.yml. The skipped component's name is given as a parameter to WithoutAutoDetectedComponents. Also note that while a component can be skipped by auto detect, it can still be added manually to the test via an option.

Example

The suite environment tries to detect the component containers to launch automatically based on the resolved configuration. E.g., if you specify the db key with driver mysql, it will add a MySQL database to the launching components. You can disable this behaviour by setting test.auto_detect.enabled to false. With disabled auto-detection, your test bed can look like this:

test:
auto_detect:
enabled: false
components:
ddb:
default:
expire_after: 4m
mysql:
default:
expire_after: 4m

We define two components of type ddb (for DynamoDB) and mysql (for a MySql server); additionally we set both their expiration times to four minutes.

Using external container instances

When booting a container takes some time, or you want to preconfigure it, it can be beneficial to fallback to a already running container instance. For this, you need to specify the option use_external_container on your component configuration, and provide connection details if they differ from the defaults. Depending on the container runtime, you need to pass host ips and ports explicitly (e.g. for docker, the IP of the bridge gateway as host).

test:
components:
mysql:
default:
use_external_container: true
host: 127.0.0.1
port: 3306

You can find a complete example inside examples/integration.

Shared environment

One of the options for a Gosoline suite integration test is suite.WithSharedEnvironment(). When this option is off, each test case will run in its own environment. For example, the fixtures are being loaded for every test case, and any change to a database or stream will only last during that test case alone. When this option is enabled, the environment is created only once and used by all the test cases, and any change done by one test case will be available to the ones who follow.