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/BeaconsHttpClient.test.ts

import { ConfigParams } from 'pip-services3-commons-node';
import { Descriptor } from 'pip-services3-commons-node';
import { References } from 'pip-services3-commons-node';

import { BeaconsMemoryPersistence } from 'pip-data-microservice-node';
import { BeaconsController } from 'pip-data-microservice-node';
import { BeaconsHttpServiceV1 } from 'pip-data-microservice-node';
import { BeaconsHttpClientV1 } from '../../src/version1/BeaconsHttpClientV1';
import { BeaconsClientV1Fixture } from './BeaconsClientV1Fixture';

suite('BeaconsHttpClientV1', () => {
    let persistence: BeaconsMemoryPersistence;
    let controller: BeaconsController;
    let service: BeaconsHttpServiceV1;
    let client: BeaconsHttpClientV1;
    let fixture: BeaconsClientV1Fixture;

    setup((done) => {
        persistence = new BeaconsMemoryPersistence();
        persistence.configure(new ConfigParams());

        controller = new BeaconsController();
        controller.configure(new ConfigParams());

        let httpConfig = ConfigParams.fromTuples(
            'connection.protocol', 'http',
            'connection.port', 3000,
            'connection.host', 'localhost'
        );

        service = new BeaconsHttpServiceV1();
        service.configure(httpConfig);

        client = new BeaconsHttpClientV1();
        client.configure(httpConfig);

        let references = References.fromTuples(
            new Descriptor('beacons', 'persistence', 'memory', 'default', '1.0'), persistence,
            new Descriptor('beacons', 'controller', 'default', 'default', '1.0'), controller,
            new Descriptor('beacons', 'service', 'http', 'default', '1.0'), service,
            new Descriptor('beacons', 'client', 'http', 'default', '1.0'), client
        );
        controller.setReferences(references);
        service.setReferences(references);
        client.setReferences(references);

        fixture = new BeaconsClientV1Fixture(client);

        try{
            await persistence.open(null);
            await service.open(null);
            await client.open(null);
        } catch (err) {
            console.log(err);
            throw err;
        }
    });

    teardown((done) => {
        await client.close(null);
        await service.close(null);
        await persistence.close(null);
        
    });

    test('CRUD Operations', (done) => {
        fixture.testCrudOperations();
    });

    test('Calculate Position', (done) => {
        fixture.testCalculatePosition();
    });

});

/test/version1/BeaconsHttpClientV1Test.py

    [Collection("Sequential")]
    public class BeaconsHttpClientV1Test : IDisposable
    {
        private static readonly ConfigParams RestConfig = ConfigParams.FromTuples(
            "connection.protocol", "http",
            "connection.host", "localhost",
            "connection.port", 3000
        );

        private BeaconsMemoryPersistence _persistence;
        private BeaconsController _controller;
        private BeaconsRestServiceV1 _service;
        private BeaconsRestClientV1 _client;
        private BeaconsClientV1Fixture _fixture;

        public BeaconsHttpClientV1Test()
        {
            _persistence = new BeaconsMemoryPersistence();
            _controller = new BeaconsController();
            _service = new BeaconsRestServiceV1();
            _client = new BeaconsRestClientV1();

            IReferences references = References.FromTuples(
                new Descriptor("beacons", "persistence", "memory", "default", "1.0"), _persistence,
                new Descriptor("beacons", "controller", "default", "default", "1.0"), _controller,
                new Descriptor("beacons", "service", "http", "default", "1.0"), _service,
                new Descriptor("beacons", "client", "http", "default", "1.0"), _client
            );

            _controller.SetReferences(references);

            _service.Configure(RestConfig);
            _service.SetReferences(references);

            _client.Configure(RestConfig);
            _client.SetReferences(references);

            _fixture = new BeaconsClientV1Fixture(_client);

            _service.OpenAsync(null).Wait();
            _client.OpenAsync(null).Wait();
        }

        public void Dispose()
        {
            _client.CloseAsync(null).Wait();
            _service.CloseAsync(null).Wait();
        }

        [Fact]
        public async Task TestCrudOperationsAsync()
        {
            await _fixture.TestCrudOperationsAsync();
        }

        [Fact]
        public async Task TestCalculatePositionsAsync()
        {
            await _fixture.TestCalculatePositionsAsync();
        }
    }

/test/version1/BeaconsHttpClientV1_test.go

package test_clients1

import (
	"testing"

	clients1 "github.com/pip-services-samples/client-beacons-gox/clients/version1"
	logic "github.com/pip-services-samples/service-beacons-gox/logic"
	persist "github.com/pip-services-samples/service-beacons-gox/persistence"
	services1 "github.com/pip-services-samples/service-beacons-gox/services/version1"
	cconf "github.com/pip-services3-gox/pip-services3-commons-gox/config"
	cref "github.com/pip-services3-gox/pip-services3-commons-gox/refer"
)

type beaconsCommandableHttpClientV1Test struct {
	persistence *persist.BeaconsMemoryPersistence
	controller  *logic.BeaconsController
	service     *services1.BeaconsHttpServiceV1
	client      *clients1.BeaconsHttpClientV1
	fixture     *BeaconsClientV1Fixture
}

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

	controller := logic.NewBeaconsController()
	controller.Configure(cconf.NewEmptyConfigParams())

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

	service := services1.NewBeaconsHttpServiceV1()
	service.Configure(httpConfig)

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

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

	fixture := NewBeaconsClientV1Fixture(client)

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

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

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

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

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

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

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

	err = c.persistence.Close("")
	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/BeaconsCommandableHttpClientV1_test.dart

import 'dart:async';

import 'package:test/test.dart';
import 'package:pip_services3_commons/pip_services3_commons.dart';
import 'package:pip_services_beacons_dart/pip_services_beacons_dart.dart';
import 'package:pip_clients_beacons_dart/pip_clients_beacons_dart.dart';

import './BeaconsClientV1Fixture.dart';

var httpConfig = ConfigParams.fromTuples([
  'connection.protocol',
  'http',
  'connection.host',
  'localhost',
  'connection.port',
  3001
]);

void main() {
  group('BeaconsCommandableHttpClientV1', () {
    BeaconsMemoryPersistence persistence;
    BeaconsController controller;
    BeaconsCommandableHttpServiceV1 service;
    BeaconsCommandableHttpClientV1 client;
    BeaconsClientV1Fixture fixture;

    setUp(() async {
      persistence = BeaconsMemoryPersistence();
      persistence.configure(ConfigParams());

      controller = BeaconsController();
      controller.configure(ConfigParams());

      service = BeaconsCommandableHttpServiceV1();
      service.configure(httpConfig);

      client = BeaconsCommandableHttpClientV1();
      client.configure(httpConfig);
      var references = References.fromTuples([
        Descriptor('beacons', 'persistence', 'memory', 'default', '1.0'),
        persistence,
        Descriptor('beacons', 'controller', 'default', 'default', '1.0'),
        controller,
        Descriptor('beacons', 'service', 'http', 'default', '1.0'),
        service,
        Descriptor('beacons', 'client', 'http', 'default', '1.0'),
        client
      ]);
      controller.setReferences(references);
      service.setReferences(references);
      client.setReferences(references);
      fixture = BeaconsClientV1Fixture(client);
      await persistence.open(null);
      await service.open(null);
      await client.open(null);
    });

    tearDown(() async {
      await client.close(null);
      await service.close(null);
      await persistence.close(null);
      await Future.delayed(Duration(milliseconds: 2000));
    });

    test('CRUD Operations', () async {
      await fixture.testCrudOperations();
    });

    test('Calculate Position', () async {
      await fixture.testCalculatePosition();
    });
  });
}


/test/version1/test_BeaconsHttpClientV1.py

from pip_services3_commons.config import ConfigParams
from pip_services3_commons.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
    controller: BeaconsController
    service: BeaconsHttpServiceV1
    client: BeaconsHttpClientV1
    fixture: BeaconsClientV1Fixture

    @classmethod
    def setup_class(cls):
        cls.controller = BeaconsController()
        cls.persistence = BeaconsMemoryPersistence()

        cls.service = BeaconsHttpServiceV1()
        cls.service.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', 'controller', 'default', 'default', '1.0'), cls.controller,
            Descriptor('beacons', 'service', 'http', 'default', '1.0'), cls.service,
            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.service.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:

npm run test

Not available

go test -v ./test/...

pub run 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.