Skip to content

Commit

Permalink
Merge pull request #16 from JulienBreux/feat/add-postman
Browse files Browse the repository at this point in the history
Allow Postman contract-testing support using ensemble design
  • Loading branch information
lbroudoux authored Feb 21, 2024
2 parents 098110a + be4e999 commit eb66723
Show file tree
Hide file tree
Showing 9 changed files with 750 additions and 225 deletions.
78 changes: 77 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,80 @@ require.False(t, testResult.Success)
require.Equal(t, "http://bad-impl:3001", testResult.TestedEndpoint)
```

The `testResult` gives you access to all details regarding success of failure on different test cases.
The `testResult` gives you access to all details regarding success of failure on different test cases.

### Advanced features with MicrocksContainersEnsemble

The `MicrocksContainer` referenced above supports essential features of Microcks provided by the main Microcks container.
The list of supported features is the following:

* Mocking of REST APIs using different kinds of artifacts,
* Contract-testing of REST APIs using `OPEN_API_SCHEMA` runner/strategy,
* Mocking and contract-testing of SOAP WebServices,
* Mocking and contract-testing of GraphQL APIs,
* Mocking and contract-testing of gRPC APIs.

To support features like Asynchronous API and `POSTMAN` contract-testing, we introduced `MicrocksContainersEnsemble` that allows managing
additional Microcks services. `MicrocksContainersEnsemble` allow you to implement
[Different levels of API contract testing](https://medium.com/@lbroudoux/different-levels-of-api-contract-testing-with-microcks-ccc0847f8c97)
in the Inner Loop with Testcontainers!

A `MicrocksContainersEnsemble` conforms to Testcontainers lifecycle methods and presents roughly the same interface
as a `MicrocksContainer`. You can create and build an ensemble that way:

```go
import (
ensemble "microcks.io/testcontainers-go/ensemble"
)

ensembleContainers, err := ensemble.RunContainers(ctx,
ensemble.WithMainArtifact("testdata/apipastries-openapi.yaml"),
ensemble.WithSecondaryArtifact("testdata/apipastries-postman-collection.json"),
)
```

A `MicrocksContainer` is wrapped by an ensemble and is still available to import artifacts and execute test methods.
You have to access it using:

```go
microcks := ensemble.GetMicrocksContainer();
microcks.ImportAsMainArtifact(...);
microcks.Logs(...);
```

Please refer to our [ensemble tests](https://github.com/microcks/microcks-testcontainers-go/blob/main/ensemble/ensemble_test.go) for comprehensive example on how to use it.

#### Postman contract-testing

On this `ensemble` you may want to enable additional features such as Postman contract-testing:

```go
import (
ensemble "microcks.io/testcontainers-go/ensemble"
)

ensembleContainers, err := ensemble.RunContainers(ctx,
// Microcks container in ensemble
ensemble.WithMainArtifact("testdata/apipastries-openapi.yaml"),
ensemble.WithSecondaryArtifact("testdata/apipastries-postman-collection.json"),

// Postman container in ensemble
ensemble.WithPostman(true),
)
```

You can execute a `POSTMAN` test using an ensemble that way:

```go
// Build a new TestRequest.
testRequest := client.TestRequest{
ServiceId: "API Pastries:0.0.1",
RunnerType: client.TestRunnerTypePOSTMAN,
TestEndpoint: "http://good-impl:3003",
Timeout: 2000,
}

testResult := ensemble.
GetMicrocksContainer().
TestEndpoint(context.Background(), testRequest);
```
183 changes: 183 additions & 0 deletions ensemble/ensemble.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package ensemble

import (
"context"
"strings"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/network"
microcks "microcks.io/testcontainers-go"
"microcks.io/testcontainers-go/ensemble/postman"
)

// Option represents an option to pass to the ensemble
type Option func(*MicrocksContainersEnsemble) error

// ContainerOptions represents the container options
type ContainerOptions struct {
list []testcontainers.ContainerCustomizer
}

// Add adds an option to the list
func (co *ContainerOptions) Add(opt testcontainers.ContainerCustomizer) {
co.list = append(co.list, opt)
}

// MicrocksContainersEnsemble represents the ensemble of containers
type MicrocksContainersEnsemble struct {
ctx context.Context

network *testcontainers.DockerNetwork

microcksContainer *microcks.MicrocksContainer
microcksContainerOptions ContainerOptions

postmanEnabled bool
postmanContainer *postman.PostmanContainer
postmanContainerOptions ContainerOptions
}

// GetNetwork returns the ensemble network
func (ec *MicrocksContainersEnsemble) GetNetwork() *testcontainers.DockerNetwork {
return ec.network
}

// GetMicrocksContainer returns the Microcks container
func (ec *MicrocksContainersEnsemble) GetMicrocksContainer() *microcks.MicrocksContainer {
return ec.microcksContainer
}

// GetPostmanContainer returns the Postman container
func (ec *MicrocksContainersEnsemble) GetPostmanContainer() *postman.PostmanContainer {
return ec.postmanContainer
}

// Terminate helps to terminate all containers
func (ec *MicrocksContainersEnsemble) Terminate(ctx context.Context) error {
// Main Microcks container
if err := ec.microcksContainer.Terminate(ctx); err != nil {
return err
}

// Postman container
if ec.postmanEnabled {
if err := ec.postmanContainer.Terminate(ctx); err != nil {
return err
}
}

return nil
}

// RunContainers creates instances of the Microcks and necessaries tools.
// Using sequential start to avoid resource contention on CI systems with weaker hardware.
func RunContainers(ctx context.Context, opts ...Option) (*MicrocksContainersEnsemble, error) {
var err error

ensemble := &MicrocksContainersEnsemble{ctx: ctx}

// Options
defaults := []Option{WithDefaultNetwork()}
options := append(defaults, opts...)
for _, opt := range options {
if err = opt(ensemble); err != nil {
return nil, err
}
}

// Microcks container
if ensemble.postmanEnabled {
postmanRunnerURL := strings.Join([]string{"http://", postman.DefaultNetworkAlias, ":3000"}, "")
ensemble.microcksContainerOptions.Add(microcks.WithEnv("POSTMAN_RUNNER_URL", postmanRunnerURL))
}
testCallbackURL := strings.Join([]string{"http://", microcks.DefaultNetworkAlias, ":8080"}, "")
ensemble.microcksContainerOptions.Add(microcks.WithEnv("TEST_CALLBACK_URL", testCallbackURL))
ensemble.microcksContainer, err = microcks.RunContainer(ctx, ensemble.microcksContainerOptions.list...)
if err != nil {
return nil, err
}

// Postman container
if ensemble.postmanEnabled {
ensemble.postmanContainer, err = postman.RunContainer(ctx, ensemble.postmanContainerOptions.list...)
if err != nil {
return nil, err
}
}

return ensemble, nil
}

// WithDefaultNetwork allows to use a default network
func WithDefaultNetwork() Option {
return func(e *MicrocksContainersEnsemble) (err error) {
e.network, err = network.New(e.ctx, network.WithCheckDuplicate())
if err != nil {
return err
}

e.microcksContainerOptions.Add(microcks.WithNetwork(e.network.Name))
e.microcksContainerOptions.Add(microcks.WithNetworkAlias(e.network.Name, microcks.DefaultNetworkAlias))
e.postmanContainerOptions.Add(postman.WithNetwork(e.network.Name))
e.postmanContainerOptions.Add(postman.WithNetworkAlias(e.network.Name, postman.DefaultNetworkAlias))

return nil
}
}

// WithNetwork allows to define the network
func WithNetwork(network *testcontainers.DockerNetwork) Option {
return func(e *MicrocksContainersEnsemble) error {
e.network = network
e.microcksContainerOptions.Add(microcks.WithNetwork(e.network.Name))
e.microcksContainerOptions.Add(microcks.WithNetworkAlias(e.network.Name, microcks.DefaultNetworkAlias))
e.postmanContainerOptions.Add(postman.WithNetwork(e.network.Name))
e.postmanContainerOptions.Add(postman.WithNetworkAlias(e.network.Name, postman.DefaultNetworkAlias))
return nil
}
}

// WithMainArtifact provides paths to artifacts that will be imported as main or main
// ones within the Microcks container.
// Once it will be started and healthy.
func WithMainArtifact(artifactFilePath string) Option {
return func(e *MicrocksContainersEnsemble) error {
e.microcksContainerOptions.Add(microcks.WithMainArtifact(artifactFilePath))
return nil
}
}

// WithSecondaryArtifact provides paths to artifacts that will be imported as main or main
// ones within the Microcks container.
// Once it will be started and healthy.
func WithSecondaryArtifact(artifactFilePath string) Option {
return func(e *MicrocksContainersEnsemble) error {
e.microcksContainerOptions.Add(microcks.WithSecondaryArtifact(artifactFilePath))
return nil
}
}

// WithPostman allows to enable Postman container
func WithPostman(enable bool) Option {
return func(e *MicrocksContainersEnsemble) error {
e.postmanEnabled = enable
return nil
}
}

// WithPostmanImage helps to use specific Postman image
func WithPostmanImage(image string) Option {
return func(e *MicrocksContainersEnsemble) error {
e.postmanContainerOptions.Add(testcontainers.WithImage(image))
e.postmanEnabled = true
return nil
}
}

// WithMicrocksImage helps to use specific Microcks image
func WithMicrocksImage(image string) Option {
return func(e *MicrocksContainersEnsemble) error {
e.postmanContainerOptions.Add(testcontainers.WithImage(image))
return nil
}
}
97 changes: 97 additions & 0 deletions ensemble/ensemble_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package ensemble_test

import (
"context"
"testing"

"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"microcks.io/testcontainers-go/ensemble"
"microcks.io/testcontainers-go/internal/test"
)

func TestMockingFunctionalityAtStartup(t *testing.T) {
ctx := context.Background()

ec, err := ensemble.RunContainers(ctx,
ensemble.WithMainArtifact("../testdata/apipastries-openapi.yaml"),
ensemble.WithSecondaryArtifact("../testdata/apipastries-postman-collection.json"),
)
require.NoError(t, err)
t.Cleanup(func() {
if err := ec.Terminate(ctx); err != nil {
t.Fatalf("failed to terminate container: %s", err)
}
})

test.ConfigRetrieval(t, ctx, ec.GetMicrocksContainer())
test.MockEndpoints(t, ctx, ec.GetMicrocksContainer())

test.MicrocksMockingFunctionality(t, ctx, ec.GetMicrocksContainer())
}

func TestPostmanContractTestingFunctionality(t *testing.T) {
ctx := context.Background()

// Ensemble
ec, err := ensemble.RunContainers(
ctx,
ensemble.WithMainArtifact("../testdata/apipastries-openapi.yaml"),
ensemble.WithSecondaryArtifact("../testdata/apipastries-postman-collection.json"),
ensemble.WithPostman(true),
)
require.NoError(t, err)
networkName := ec.GetNetwork().Name

// Demo pastry bad implementation
badImpl, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "quay.io/microcks/contract-testing-demo:02",
Networks: []string{networkName},
NetworkAliases: map[string][]string{
networkName: {"bad-impl"},
},
WaitingFor: wait.ForLog("Example app listening on port 3002"),
},
Started: true,
})
require.NoError(t, err)

// Demo pastry good implementation
goodImpl, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "quay.io/microcks/contract-testing-demo:03",
Networks: []string{networkName},
NetworkAliases: map[string][]string{
networkName: {"good-impl"},
},
WaitingFor: wait.ForLog("Example app listening on port 3003"),
},
Started: true,
})
require.NoError(t, err)

// Cleanup containers
t.Cleanup(func() {
if err := ec.GetMicrocksContainer().Terminate(ctx); err != nil {
t.Fatalf("failed to terminate container: %s", err)
}
if err := badImpl.Terminate(ctx); err != nil {
t.Fatalf("failed to terminate container: %s", err)
}
if err := goodImpl.Terminate(ctx); err != nil {
t.Fatalf("failed to terminate container: %s", err)
}
})

// Tests & assertions
test.ConfigRetrieval(t, ctx, ec.GetMicrocksContainer())
test.MicrocksContractTestingFunctionality(
t,
ctx,
ec.GetMicrocksContainer(),
badImpl,
goodImpl,
)
}
Loading

0 comments on commit eb66723

Please sign in to comment.