Step 6. Testing the Client with a Remote Microservice

In the previous steps, we were looking at the development of clients and at the clients themselves. However, both the client and the microservice were implemented in the same language, which allowed us to run components of the microservice from right inside the tests. In a multi-language environment, there are times when you need to create clients in a language that is different from the one that was used to write the microservice. In this last step, we will be demonstrating a similar situation, showing how to use Docker to link and run microservices and other infrastructure services, and describing how to integrate this into the development process. This step will be using the BeaconsCommandableHttpClientV1 class that we created in step 3, along with a new set of tests that still uses the Beacons microservice, but this time - one that is running in a separate Docker container. In other words, the Beacon microservice that is on DockerHub is written in Node.js, which may or may not be the language that you have been using throughout this tutorial. To run the microservice in Docker, we’ll first need to create a configuration file called docker/docker-compose.dev.yml:

version: '3.3'
services:
  app:    
    image: "pipdevs/data-microservice-node:1.0"    
    ports:      
      - "8080:8080"  
    depends_on:      
      - mongo     
    environment:      
      - HTTP_ENABLED=true      
      - HTTP_PORT=8080      
      - MONGO_ENABLED=true      
      - MONGO_SERVICE_HOST=mongo      
      - MONGO_SERVICE_PORT=27017
  mongo:    
    image: mongo:latest      
    ports:        
      - "27017:27017"

This configuration will run the microservice alongside a Mongo DB for data storage. HTTP services will also be started on port 8080 and will be made accessible from outside the container. You can set up the microservice to run with a memory persistence as well. To do this, comment out the MONGO environment variables, as well as any other Mongo-related parameters. The image parameter contains the name of the Docker image being hosted on DockerHub (pipdevs/data-microservice-node:1.0), which is the microservice we want to be testing with. Once we get our microservice up and running, it will be available at http://localhost:8080, and the client will be able to work with it using this address. Let’s design a test, in which the client will work with our new service. We’ll be basing this test off of the one we wrote in Step 3. Place the code below into a file named test_BeaconsHttpClientV1.py.

/test/version1/BeaconsHttpClientV1_test.go

package test_clients1

import (
	"context"
	"testing"

	clients1 "github.com/pip-services-samples/client-beacons-go/clients/version1"
	controller1 "github.com/pip-services-samples/service-beacons-go/controllers/version1"
	persist "github.com/pip-services-samples/service-beacons-go/persistence"
	logic "github.com/pip-services-samples/service-beacons-go/service"
	cconf "github.com/pip-services4/pip-services4-go/pip-services4-components-go/config"
	cref "github.com/pip-services4/pip-services4-go/pip-services4-components-go/refer"
)

type beaconsCommandableHttpClientV1Test struct {
	persistence *persist.BeaconsMemoryPersistence
	service     *logic.BeaconsService
	controller  *controller1.BeaconsHttpControllerV1
	client      *clients1.BeaconsHttpClientV1
	fixture     *BeaconsClientV1Fixture
	ctx         context.Context
}

func newBeaconsHttpClientV1Test() *beaconsCommandableHttpClientV1Test {
	ctx := context.Background()
	persistence := persist.NewBeaconsMemoryPersistence()
	persistence.Configure(ctx, cconf.NewEmptyConfigParams())

	service := logic.NewBeaconsService()
	service.Configure(ctx, cconf.NewEmptyConfigParams())

	httpConfig := cconf.NewConfigParamsFromTuples(
		"connection.protocol", "http",
		"connection.port", "3000",
		"connection.host", "localhost",
	)

	controller := controller1.NewBeaconsHttpControllerV1()
	controller.Configure(ctx, httpConfig)

	client := clients1.NewBeaconsHttpClientV1()
	client.Configure(ctx, httpConfig)

	references := cref.NewReferencesFromTuples(ctx,
		cref.NewDescriptor("beacons", "persistence", "memory", "default", "1.0"), persistence,
		cref.NewDescriptor("beacons", "service", "default", "default", "1.0"), service,
		cref.NewDescriptor("beacons", "controller", "http", "default", "1.0"), controller,
		cref.NewDescriptor("beacons", "client", "http", "default", "1.0"), client,
	)
	service.SetReferences(ctx, references)
	controller.SetReferences(ctx, references)
	client.SetReferences(ctx, references)

	fixture := NewBeaconsClientV1Fixture(client)

	return &beaconsCommandableHttpClientV1Test{
		persistence: persistence,
		controller:  controller,
		service:     service,
		client:      client,
		fixture:     fixture,
		ctx:         ctx,
	}
}

func (c *beaconsCommandableHttpClientV1Test) setup(t *testing.T) {
	err := c.persistence.Open(c.ctx)
	if err != nil {
		t.Error("Failed to open persistence", err)
	}

	err = c.controller.Open(c.ctx)
	if err != nil {
		t.Error("Failed to open service", err)
	}

	err = c.client.Open(c.ctx)
	if err != nil {
		t.Error("Failed to open client", err)
	}

	err = c.persistence.Clear(c.ctx)
	if err != nil {
		t.Error("Failed to clear persistence", err)
	}
}

func (c *beaconsCommandableHttpClientV1Test) teardown(t *testing.T) {
	err := c.client.Close(c.ctx)
	if err != nil {
		t.Error("Failed to close client", err)
	}

	err = c.controller.Close(c.ctx)
	if err != nil {
		t.Error("Failed to close service", err)
	}

	err = c.persistence.Close(c.ctx)
	if err != nil {
		t.Error("Failed to close persistence", err)
	}
}

func TestBeaconsHttpClientV1(t *testing.T) {
	c := newBeaconsHttpClientV1Test()

	c.setup(t)

	t.Run("CRUD Operations", c.fixture.TestCrudOperations)
	c.teardown(t)

	c.setup(t)

	t.Run("Calculate Positions", c.fixture.TestCalculatePosition)
	c.teardown(t)
}

/test/version1/test_BeaconsHttpClientV1.py

from pip_services4_components.config import ConfigParams
from pip_services4_components.refer import References, Descriptor

#from src.clients.version1.BeaconsHttpClientV1 import BeaconsHttpClientV1
#from src.logic.BeaconsController import BeaconsController
#from src.persistence.BeaconsMemoryPersistence import BeaconsMemoryPersistence
#from src.services.version1.BeaconsHttpServiceV1 import BeaconsHttpServiceV1
#from test.clients.version1.BeaconsClientV1Fixture import BeaconsClientV1Fixture

http_config = ConfigParams.from_tuples(
    'connection.protocol', 'http',
    'connection.port', 3000,
    'connection.host', 'localhost')


class TestBeaconsHttpClientV1:
    persistence: BeaconsMemoryPersistence
    service: BeaconsService
    controller: BeaconsHttpControllerV1
    client: BeaconsHttpClientV1
    fixture: BeaconsClientV1Fixture

    @classmethod
    def setup_class(cls):
        cls.service = BeaconsService()
        cls.persistence = BeaconsMemoryPersistence()

        cls.controller = BeaconsHttpControllerV1()
        cls.controller.configure(http_config)

        cls.client = BeaconsHttpClientV1()
        cls.client.configure(http_config)

        cls.references = References.from_tuples(
            Descriptor('beacons', 'persistence', 'memory', 'default', '1.0'), cls.persistence,
            Descriptor('beacons', 'service', 'default', 'default', '1.0'), cls.service,
            Descriptor('beacons', 'controller', 'http', 'default', '1.0'), cls.controller,
            Descriptor('beacons', 'client', 'http', 'default', '1.0'), cls.client
        )
        cls.controller.set_references(cls.references)
        cls.client.set_references(cls.references)
        cls.service.set_references(cls.references)

        cls.fixture = BeaconsClientV1Fixture(cls.client)

        cls.persistence.open(None)
        cls.controller.open(None)
        cls.client.open(None)

    @classmethod
    def teardown_class(cls):
        cls.client.close(None)
        cls.service.close(None)
        cls.persistence.close(None)

    def test_crud_operations(self):
        self.fixture.test_crud_operations()

    def test_calculate_position(self):
        self.fixture.test_calculate_position()

Not available

This test differs from the previous one mainly in that we aren’t running the microservice’s components in the test itself. Instead, we are configuring our client to connect to the microservice, which will be running in our Docker container. Another difference is that we will be deleting all data from the microservice before each test, so that our test always starts off with a clean DB. Before we can start testing, we need to get our containerized microservice up and running. To do this, run the command:

docker-compose -f ./docker/docker-compose.dev.yml up 

In a separate console, run the test using the following command:

go test -v ./test/...

python test.py

Not available

The tests should all pass, and the container’s console should display information about what it was doing in the process:

[beacons:INFO:2020-06-24T15:27:42.747Z] Press Control-C to stop the microservice...
[beacons:DEBUG:2020-06-24T15:27:42.982Z] Opened REST service at http://0.0.0.0:8080
[beacons:INFO:2020-06-24T15:27:42.983Z] Container beacons started.
[123:TRACE:2020-06-24T15:31:19.003Z] Executing v1/beacons.create_beacon method
[123:TRACE:2020-06-24T15:31:19.007Z] Created item 1
[123:TRACE:2020-06-24T15:31:19.093Z] Executing v1/beacons.create_beacon method
[123:TRACE:2020-06-24T15:31:19.093Z] Created item 2
[123:TRACE:2020-06-24T15:31:19.108Z] Executing v1/beacons.get_beacons method
[123:TRACE:2020-06-24T15:31:19.111Z] Retrieved 2 items
[123:TRACE:2020-06-24T15:31:19.165Z] Executing v1/beacons.update_beacon method
[123:TRACE:2020-06-24T15:31:19.167Z] Updated item 1
[123:TRACE:2020-06-24T15:31:19.218Z] Executing v1/beacons.get_beacon_by_udi method
[123:TRACE:2020-06-24T15:31:19.220Z] Found beacon by 00001
[123:TRACE:2020-06-24T15:31:19.270Z] Executing v1/beacons.delete_beacon_by_id method
[123:TRACE:2020-06-24T15:31:19.271Z] Deleted item by 1
[123:TRACE:2020-06-24T15:31:19.322Z] Executing v1/beacons.get_beacon_by_id method
[123:TRACE:2020-06-24T15:31:19.322Z] Cannot find item by 1
[123:TRACE:2020-06-24T15:31:19.332Z] Executing v1/beacons.delete_beacon_by_id method
[123:TRACE:2020-06-24T15:31:19.333Z] Deleted item by 2
[123:TRACE:2020-06-24T15:31:21.435Z] Executing v1/beacons.create_beacon method
[123:TRACE:2020-06-24T15:31:21.435Z] Created item 1
[123:TRACE:2020-06-24T15:31:21.448Z] Executing v1/beacons.create_beacon method
[123:TRACE:2020-06-24T15:31:21.449Z] Created item 2
[123:TRACE:2020-06-24T15:31:21.505Z] Executing v1/beacons.calculate_position method
[123:TRACE:2020-06-24T15:31:21.509Z] Retrieved 1 items
[123:TRACE:2020-06-24T15:31:21.562Z] Executing v1/beacons.calculate_position method
[123:TRACE:2020-06-24T15:31:21.563Z] Retrieved 2 items

Using this approach, any combination of services and clients can be tested against one another.