go-bdd-common is a generic bdd tool helping to write scalable tests for microservices applications. The framework is implemented in golang using godog.
It runs within Docker Compose allowing you to setup and a tear down a deterministic known state environment within which to run your integration tests.
It supports 3 transports at the moment:
- Http
- Grpc
- Kafka
These allow you to send and receive data to services and test the responses, headers and response codes they may return.
It uses templated json files for request data and can validate responses against json files.
In order to use the library, add it to your project:
go get github.com/libsv/go-bdd-common
We recommend adding to the service you are testing to keep everything together and adding a testing
folder at the root with an integration
sub folder. This
will allow you to keep all your tests in one place and keep the integration tests separate.
Setup the below folder structure
- testing
- integration
- data (for json request files)
- features (for .feature files)
- responses (for json responses)
- env.yml (see below, allows you to setup real / mocked services)
- integration_test.go (test entry point)
- integration
This is the main entry point to the tests and is picked up when running go test, note the build flag at the top.
This ensures the tests are only ran when you explicitly want to run them, otherwise, when running unit tests the integration suite would also be spun up.
// +build godog
package integration
import (
"os"
"testing"
cgodog "github.com/libsv/go-bdd-common/godog"
)
func TestMain(t *testing.M) {
exitCode := 1
defer func() { os.Exit(exitCode) }()
exitCode = cgodog.NewSuite(cgodog.Options{
ServiceName: "service-name", // your service
ServicePort: ":1234", // your port
InitBitcoinBackend: true, // do we want bitcoin node running
InitS3: true, // do we want to setup minio
InitKafka: true, // initialise kafka
}).Run()
}
You can choose to run real or mocked services when running the integration tests.
NOTE: these services must exist in the docker compose file you are using, if you supply a service that isn't present, it will fail.
At the root of your integration test folder, add an env.yml
file.
An example is shown:
real:
- s3
- kafka
mocks:
- this-service
- that-service
vars:
- MY_ENV_VAR=true
Note the 3 sections:
- real - actual services to run, these will be setup as docker containers
- mocks - mockec services, we can setup predicates for these to return canned responses
- vars - custom environment variables
These services must exist in the docker compose file being used.
Being in this section means the tests will setup a container for this service and run the actual code.
Useful if you NEED to run another service your service depends on like kafka, sql etc.
As much as possible, you want to exercise the service being tested only, this is where you can then mock downstream dependencies.
For example, given the below dependency:
my-service -> other-service
We want to test my-service but don't want to test other-service as this is a test for my-service.
By mocking other-service you ensure that a failure in other-service won't impact my-service.
BUT by mocking, you can FORCE failures to check how my-service works. For example, for a request you could send a header or body data, setup the mock to return a 500 and test your servic ereacts properly.
Add the mock .json files to the /mock folder.
We recommend a file per mocked service, but you can split these as required.
The server we used is based off grpc-mock.
Some examples can be seen by following the above link.
NOTE: only grpc mocking is supported right now.
These are environment variables and they are passed ONLY to the service being tested.
The pre-canned steps are documented below.
You can of course add your own specific step definitions as required.
These are hooked into the cgodog.NewSuite(
call in integration_tests.go
Our general steps should cover most scenarios, but of course there are edge cases and specifics that don't make sense here but you need.
The hook is an initScenerio function, the signature is shown below and is ran before each scenario is ran:
func initScenario(sCtx *godog.ScenarioContext, tCtx *cgodog.Context)
You can then add to the initSuite call as shown:
cgodog.NewSuite(cgodog.Options{
ServiceName: "my-service",
ServicePort: ":1234",
ScenarioInitializer: initScenario, // add function hook
}).Run())
func initScenario(sCtx *godog.ScenarioContext, tCtx *cgodog.Context) {
// can also add some pre-scenario code that is ran before every scenario is ran
ctx.Step(`^I make a RANDOM request to "([^"]*)"$`, IMakeARANDOMRequestTo(testCtx))
}
// IMakeARANDOMRequestTo
func IMakeARANDOMRequestTo(ctx *Context) func(string) error {
return func(proto string) error {
// implementation of step
}
}
You can then add your new function to the feature files in the service.
There are a number of pre-canned steps ready to use, these should cover most basic tests required when testing microservices.
Step | Description | Example |
---|---|---|
I make a GRPC request to "" | Sends a GRPC request, with an empty body | I make a GRPC request to "proto.MyService/GetThing" |
I make a GRPC request to "" with JSON "" | Sends a GRPC request, with a json body, read from data/{filename}.json | I make a GRPC request to "proto.MyService/AddThing" with JSON "the_request" |
I make a GRPC request to "" on port 1234 with JSON "" | Sends a GRPC request, with a json body, read from data/{filename}.json to a specific port | I make a GRPC request to "proto.MyService/AddThing" on port 1234 with JSON "the_request" |
the GRPC code should be xx | Ran after one of the above steps, will check the GRPC response code matches the one supplied | the GRPC code should be OK - codes are listed https://grpc.github.io/grpc/core/md_doc_statuscodes.html |
Step | Description | Example |
---|---|---|
I make a (GET POST PATCH DELETE) request to "" | Sends an HTTP request with the supplied method to the endpoint defined. | I make a GET request to "/api/v1/endpoint" |
I make a (GET POST PATCH DELETE) request to "" with JSON "" | Sends an HTTP request with the supplied method to the endpoint defined with a request body defined in a JSON file | I make a POST request to "/api/v1/endpoint" with JSON "my_request" |
the HTTP response code should be 000 | Added after one of the above steps, will validate the http response code matches what we expect | the HTTP response code should be 201 - one of https://httpstatuses.com/ |
Step | Description | Example |
---|---|---|
there (are is) ([0-9]+) bitcoin account(s)?$ | Will setup a bitcoin node with n accounts and assign them to ctx.BTC.Accounts | there are 4 bitcoin accounts |
I send ([0-9.]+) BTC to account ([0-9]+)$ | Sends the supplied about of bitcoin to the account with index | I send 0.001 BTC to account 1 |
account ([0-9]+) sends ([0-9.]+) BTC to account ([0-9]+)$ | Send n number of bitcoin to account index from account | account 0 sends 0.1 BTC to account 1 |
I send BTC to accounts: | A table that defines account aliases and amount to top them up with |
And I send BTC to accounts:
| alias | amount |
| unused1 | 2 |
| unused2 | 3 |
Step | Description | Example |
---|---|---|
there (are is) ([0-9]+) bitcoin account(s)?$ | Will setup a bitcoin node with n accounts and assign them to ctx.BTC.Accounts | there are 4 bitcoin accounts |
I send ([0-9.]+) BTC to account ([0-9]+)$ | Sends the supplied about of bitcoin to the account with index | I send 0.001 BTC to account 1 |
account ([0-9]+) sends ([0-9.]+) BTC to account ([0-9]+)$ | Send n number of bitcoin to account index from account | account 0 sends 0.1 BTC to account 1 |
I send BTC to accounts: | A table that defines account aliases and amount to top them up with |
Database files should be stored under the /sql folder with the suffix .sql. The suffix does not need to be added to the name in the below steps
Step | Description | Example |
---|---|---|
the database is seeded with "([^"]*) | Run the named sql file against the database, useful for seeding a database with known state | Give the database is seeded with "bad_data" |
Any files content checks will look for a folder named s3 in the root of the Integration test folder. Unlike json and sql files, suffix is required here as there could be different file content types we are checking.
Step | Description | Example |
---|---|---|
I delete from s3: | Remove a list of file names from S3 | Give I delete from s3: (full example below) |
the file "([^"]+)" should exist in s3 | Checks that a file with the key provided exists, this can be of the form of a file path | And the file "test/thing" should exist in s3 |
the file "([^"]+)" in s3 contains the content of "([^"]+)" | Checks that a file at path has content matching the file provided. These files should be added to an s3 folder and include the suffix | And the file "test/thing" in s3 contains the content of "post_tx.txt" |
Step | Description | Example |
---|---|---|
I send a Kafka message to topic "([^"])" with JSON "([^"])" | Publishes the supplied JSON content to the kafka topic provided. Messages sent are given a unique identifier allowing the value to be checked by the below step as the identifier propagates | When I send a Kafka message to topic "test.topic" with JSON "my_data" |
I listen to topic "([^"]*)" and wait (\d+) ms for our message | Sets up a listener on the provided topic and can be given a configurable wait. | And I listen to topic "test.response" and wait 5000 ms for our message |
Step | Description | Example |
---|---|---|
the headers: | Adds key value pairs to all outgoing requests following | Given the headers: (example below) |
I delete the headers: | A list of header names to remove | I delete the headers: |
the template values: | Key value pairs of values to write to use in request or responses | Given the template values: |
I store from the response for templating: | Used for responses, can supply a JSON path and a name to give it, this can then be referenced in responses | And I store from the response for templating: |
the data should match JSON "([^"]*)" | Will check a response (from http, grpc or kafka) matches the json at the file named. | Then I listen to topic "test.accepted" and wait 2000 ms for our message And the data should match JSON "my_expected" |
I wait for (\d+) second(s) | Pauses the current scenario for n seconds | And I wait for 1 second |
Example for the headers:
And the headers:
| key | val |
| header1 | abc |
| header2 | integration-tx-broadcast |
Example
And I delete the headers:
| key |
| header1 |
| header2 |
Example
This will write the value 100 to the variable named balance as shown:
Given I fund 100 satoshis
And the template values:
| key | val |
| balance | 100 |
This variable can then be referenced in data or response json files like this:
{
"myprop": "test",
"balance": "{{ .balance }}"
}
If you need to check the same response template over and over with different values, this is a convenient way of doing it
And I store from the response for templating:
| jsonpath | name |
| body.TxBytes | transaction |
This stores a value in the json response names TxBytes to a new variable called transaction.
This can then be referenced in json files as shown:
{
"myprop": "test",
"tx": "{{ .transaction }}"
}