Step 5. Implementing a Mock Client

Complex systems usually experience difficulties when it comes to writing unit tests for a logic that calls external services. These tests are supposed to run quickly and without any additional infrastructure. The standard approach to solving this problem is to replace the calls to external services with a local approximation (a.k.a. a mock). However, writing mocks takes time and doesn’t always guarantee functionality that matches the real service.

In our projects, we’ve come to the conclusion that it pays off to develop mocks alongside the real clients and test them using common tests, to guarantee that their behavior is identical. This way, all users of the microservice will receive both the client and mock from the library and will be able to start coding logic and unit tests for it without delay.

In this step we will be demonstrating how Mock clients are developed and how they can be tested using the tests we created earlier.

The test client has to implement the same interface that the other clients did. However, the client’s methods are going to contain code that only imitates the microservice’s behavior.

The code for this client is showed below:

/src/version1/BeaconsMockClientV1.ts


let _ = require('lodash');
let async = require('async');

import { FilterParams, IdGenerator } from 'pip-services3-commons-node';
import { PagingParams } from 'pip-services3-commons-node';
import { DataPage } from 'pip-services3-commons-node';

import { BeaconV1 } from './BeaconV1';
import { IBeaconsClientV1 } from './IBeaconsClientV1';

export class BeaconsMockClientV1 implements IBeaconsClientV1 {
    private _maxPageSize: number = 100;
    private _items: BeaconV1[];

    public constructor(...items: BeaconV1[]) {
        this._items = items;
    }

    private composeFilter(filter: FilterParams): any {
        filter = filter || new FilterParams();

        let id = filter.getAsNullableString('id');
        let siteId = filter.getAsNullableString('site_id');
        let label = filter.getAsNullableString('label');
        let udi = filter.getAsNullableString('udi');
        let udis = filter.getAsObject('udis');
        if (_.isString(udis))
            udis = udis.split(',');
        if (!_.isArray(udis))
            udis = null;

        return (item) => {
            if (id != null && item.id != id)
                return false;
            if (siteId != null && item.site_id != siteId)
                return false;
            if (label != null && item.label != label)
                return false;
            if (udi != null && item.udi != udi)
                return false;
            if (udis != null && _.indexOf(udis, item.udi) < 0)
                return false;
            return true;
        };
    }

    public async getBeacons(correlationId: string, filter: FilterParams, paging: PagingParams): Promise<DataPage<BeaconV1>> {
        let filterBeacons = this.composeFilter(filter);
        let beacons = _.filter(this._items, filterBeacons);

        // Extract a page
        paging = paging != null ? paging : new PagingParams();
        let skip = paging.getSkip(-1);
        let take = paging.getTake(this._maxPageSize);

        let total = null;
        if (paging.total)
            total = beacons.length;

        if (skip > 0)
            beacons = _.slice(beacons, skip);
        beacons = _.take(beacons, take);

        return new DataPage<BeaconV1>(beacons, total);
    }

    public async getBeaconById(correlationId: string, beaconId: string): Promise<BeaconV1> {
        let beacons = this._items.filter((x) => { return x.id == beaconId; });
        let beacon = beacons.length > 0 ? beacons[0] : null;

        return beacon;
    }

    public async getBeaconByUdi(correlationId: string, udi: string): Promise<BeaconV1> {
        let beacons = this._items.filter((x) => { return x.udi == udi; });
        let beacon = beacons.length > 0 ? beacons[0] : null;

        return beacon;
    }

    public async calculatePosition(correlationId: string, siteId: string, udis: string[]): Promise<BeaconV1> {
        let beacons: BeaconV1[];
        let position: any = null;

        if (udis == null || udis.length == 0) {
            return;
        }

        let page = await this.getBeacons(
                    correlationId,
                    FilterParams.fromTuples(
                        'site_id', siteId,
                        'udis', udis
                    ),
                    null,
                );
                
        let beacons = page ? page.data : [];

        let lat = 0;
        let lng = 0;
        let count = 0;

        for (let beacon of beacons) {
            if (beacon.center != null
                && beacon.center.type == 'Point'
                && _.isArray(beacon.center.coordinates)) {
                lng += beacon.center.coordinates[0];
                lat += beacon.center.coordinates[1];
                count += 1;
            }
        }

        if (count > 0) {
            position = {
                type: 'Point',
                coordinates: [lng / count, lat / count]
            }
        }

        return position
    }

    public async createBeacon(correlationId: string, beacon: BeaconV1): Promise<BeaconV1> {
        if (beacon == null) {
            return;
        }

        beacon = _.clone(beacon);
        beacon.id = beacon.id || IdGenerator.nextLong();

        this._items.push(beacon);

        return beacon
    }

    public async updateBeacon(correlationId: string, beacon: BeaconV1): Promise<BeaconV1> {
        let index = this._items.map((x) => { return x.id; }).indexOf(beacon.id);

        if (index < 0) {
            return;
        }

        beacon = _.clone(beacon);
        this._items[index] = beacon;
        return beacon;
    }

    public async deleteBeaconById(correlationId: string, beaconId: string): Promise<BeaconV1> {
        var index = this._items.map((x) => { return x.id; }).indexOf(beaconId);
        var item = this._items[index];

        if (index < 0) {
            return;
        }
        this._items.splice(index, 1);
        return item;
    }
}

/src/version1/BeaconsMockClientV1.cs


    public class BeaconsMockClientV1 : IBeaconsClientV1
    {
        protected int maxPageSize = 100;
        protected List<BeaconV1> _items = new List<BeaconV1>();

        private Func<BeaconV1, bool> ComposeFilter(FilterParams filter)
        {
            filter = filter ?? new FilterParams();

            var id = filter.GetAsNullableString("id");
            var siteId = filter.GetAsNullableString("site_id");
            var label = filter.GetAsNullableString("label");
            var udi = filter.GetAsNullableString("udi");

            var udis = filter.GetAsNullableString("udis");
            var udiList = udis != null ? udis.Split(',') : null;

            return (item) =>
                {
                    if (id != null && item.Id != id)
                        return false;
                    if (siteId != null && item.SiteId != siteId)
                        return false;
                    if (label != null && item.Label != label)
                        return false;
                    if (udi != null && item.Udi != udi)
                        return false;
                    if (udiList != null && !udiList.Contains(item.Udi))
                        return false;
                    return true;
                };

        }

        public async Task<DataPage<BeaconV1>> GetBeaconsAsync(string correlationId, FilterParams filter, PagingParams paging)
        {
            var filterBeacons = ComposeFilter(filter);

            var beacons = _items.FindAll(beacon => filterBeacons(beacon));

            paging = paging ?? new PagingParams();

            var skip = Convert.ToInt32(paging.GetSkip(-1));
            var take = Convert.ToInt32(paging.GetTake(maxPageSize));
            long? total = null;

            if (paging.Total)
                total = beacons.Count();
            if (skip > 0)
                beacons = beacons.Skip(skip).Take(take).ToList();

            var page = await Task.FromResult(new DataPage<BeaconV1>(beacons, total));

            return page;
        }

        public async Task<BeaconV1> GetBeaconByIdAsync(string correlationId, string id)
        {
            var beacons = await Task.FromResult(_items.Find(beacon => beacon.Id == id));

            return beacons;
        }

        public async Task<BeaconV1> GetBeaconByUdiAsync(string correlationId, string udi)
        {
            var beacons = await Task.FromResult(_items.Find(beacon => beacon.Udi == udi));

            return beacons;
        }

        public async Task<CenterObjectV1> CalculatePositionAsync(string correlationId, string siteId, string[] udis)
        {
            List<BeaconV1> beacons;

            var page = await GetBeaconsAsync(correlationId, FilterParams.FromTuples(
                "site_id", siteId, "udis", udis), null);

            beacons = page.Data ?? new List<BeaconV1>();

            double lat = 0;
            double lng = 0;
            double count = 0;

            foreach (BeaconV1 beacon in beacons)
            {
                if (beacon.Center != null && beacon.Center.Type == "Point" && beacon.Center.Coordinates.Length > 1)
                {
                    lng = lng + beacon.Center.Coordinates[0];
                    lat = lat + beacon.Center.Coordinates[1];
                    count = count + 1;
                }
            }

            if (count > 0)
                return new CenterObjectV1()
                {
                    Coordinates = new double[] { lng / count, lat / count },
                    Type = "Point"

                };

            return null;
        }

        public async Task<BeaconV1> CreateBeaconAsync(string correlationId, BeaconV1 beacon)
        {
            if (beacon == null) return null;

            beacon.Id = beacon.Id ?? IdGenerator.NextLong();
            _items.Add(beacon);

            return await Task.FromResult(beacon);
        }

        public async Task<BeaconV1> UpdateBeaconAsync(string correlationId, BeaconV1 beacon)
        {
            var index = _items.FindIndex(el => el.Id == beacon.Id);

            if (index < 0) return null;

            _items[index] = beacon;

            return await Task.FromResult(beacon);

        }

        public async Task<BeaconV1> DeleteBeaconByIdAsync(string correlationId, string id)
        {
            var index = _items.FindIndex(el => el.Id == id);

            if (index < 0) return null;

            var deletedBeacon = _items[index];

            _items.RemoveAt(index);

            return await Task.FromResult(deletedBeacon);
        }
    }
    

/version1/BeaconsMemoryClientV1.go

package clients1

import (
	"context"
	"reflect"
	"strings"

	data1 "github.com/pip-services-samples/service-beacons-gox/data/version1"
	cdata "github.com/pip-services3-gox/pip-services3-commons-gox/data"
	mdata "github.com/pip-services3-gox/pip-services3-data-gox/persistence"
)

type BeaconsMemoryClientV1 struct {
	maxPageSize int
	items       []data1.BeaconV1
	proto       reflect.Type
}

func NewBeaconsMemoryClientV1(items []data1.BeaconV1) *BeaconsMemoryClientV1 {
	c := &BeaconsMemoryClientV1{
		maxPageSize: 100,
		items:       make([]data1.BeaconV1, 0),
		proto:       reflect.TypeOf(data1.BeaconV1{}),
	}
	c.items = append(c.items, items...)
	return c
}

func (c *BeaconsMemoryClientV1) composeFilter(filter cdata.FilterParams) func(item data1.BeaconV1) bool {

	id := filter.GetAsString("id")
	siteId := filter.GetAsString("site_id")
	label := filter.GetAsString("label")
	udi := filter.GetAsString("udi")
	udis := filter.GetAsString("udis")

	var udiValues []string
	if udis != "" {
		udiValues = strings.Split(udis, ",")
	}

	return func(item data1.BeaconV1) bool {

		if id != "" && item.Id != id {
			return false
		}
		if siteId != "" && item.SiteId != siteId {
			return false
		}
		if label != "" && item.Label != label {
			return false
		}
		if udi != "" && item.Udi != udi {
			return false
		}
		if len(udiValues) > 0 && strings.Index(udis, item.Udi) < 0 {
			return false
		}
		return true
	}
}

func (c *BeaconsMemoryClientV1) GetBeacons(ctx context.Context,
	correlationId string, filter cdata.FilterParams, paging cdata.PagingParams) (page *cdata.DataPage[data1.BeaconV1], err error) {
	filterBeacons := c.composeFilter(filter)

	beacons := make([]data1.BeaconV1, 0)
	for _, v := range c.items {
		if filterBeacons(v) {
			item := v
			beacons = append(beacons, item)
		}
	}

	skip := paging.GetSkip(-1)
	take := paging.GetTake((int64)(c.maxPageSize))
	var total int = 0

	if paging.Total {
		total = (len(beacons))
	}

	if skip > 0 {
		beacons = beacons[skip:]
	}

	if (int64)(len(beacons)) >= take {
		beacons = beacons[:take]
	}

	return cdata.NewDataPage(beacons, total), nil
}

func (c *BeaconsMemoryClientV1) GetBeaconById(ctx context.Context,
	correlationId string, beaconId string) (beacon *data1.BeaconV1, err error) {

	var item *data1.BeaconV1
	for _, v := range c.items {
		if v.Id == beaconId {
			item = &v
			break
		}
	}

	return item, nil
}

func (c *BeaconsMemoryClientV1) GetBeaconByUdi(ctx context.Context,
	correlationId string, udi string) (beacon *data1.BeaconV1, err error) {
	var item *data1.BeaconV1
	for _, v := range c.items {
		if v.Udi == udi {
			item = &v
			break
		}
	}

	return item, nil
}

func (c *BeaconsMemoryClientV1) CalculatePosition(ctx context.Context,
	correlationId string, siteId string, udis []string) (*data1.GeoPointV1, error) {

	if udis == nil || len(udis) == 0 {
		return nil, nil
	}

	page, err := c.GetBeacons(ctx,
		correlationId, *cdata.NewFilterParamsFromTuples(
			"site_id", siteId,
			"udis", udis),
		*cdata.NewEmptyPagingParams())

	if err != nil || page == nil {
		return nil, err
	}

	var lat float32 = 0
	var lng float32 = 0
	var count = 0

	for _, beacon := range page.Data {
		if beacon.Center.Type == "Point" {
			lng += beacon.Center.Coordinates[0]
			lat += beacon.Center.Coordinates[1]
			count += 1
		}
	}

	pos := data1.GeoPointV1{
		Type:        "Point",
		Coordinates: make([]float32, 2, 2),
	}

	if count > 0 {
		pos.Type = "Point"
		pos.Coordinates[0] = lng / (float32)(count)
		pos.Coordinates[1] = lat / (float32)(count)
	}

	return &pos, nil
}

func (c *BeaconsMemoryClientV1) CreateBeacon(ctx context.Context,
	correlationId string, beacon data1.BeaconV1) (res *data1.BeaconV1, err error) {

	newItem := mdata.CloneObject(beacon, c.proto)
	item, _ := newItem.(data1.BeaconV1)
	mdata.GenerateObjectId(&newItem)

	c.items = append(c.items, item)
	return &item, nil
}

func (c *BeaconsMemoryClientV1) UpdateBeacon(ctx context.Context,
	correlationId string, beacon data1.BeaconV1) (res *data1.BeaconV1, err error) {

	var index = -1
	for i, v := range c.items {
		if v.Id == beacon.Id {
			index = i
			break
		}
	}

	if index < 0 {
		return nil, nil
	}

	newItem := mdata.CloneObject(beacon, c.proto)
	item, _ := newItem.(data1.BeaconV1)
	c.items[index] = item
	return &item, nil
}

func (c *BeaconsMemoryClientV1) DeleteBeaconById(ctx context.Context,
	correlationId string, beaconId string) (res *data1.BeaconV1, err error) {

	var index = -1
	for i, v := range c.items {
		if v.Id == beaconId {
			index = i
			break
		}
	}

	if index < 0 {
		return nil, nil
	}

	var item = c.items[index]

	if index == len(c.items) {
		c.items = c.items[:index-1]
	} else {
		c.items = append(c.items[:index], c.items[index+1:]...)
	}
	return &item, nil
}

/lib/version1/BeaconsMockClientV1.dart

import 'dart:async';
import 'dart:convert';

import 'package:pip_services3_commons/pip_services3_commons.dart';

import '../../clients/version1/clients.dart';
import '../../data/version1/BeaconV1.dart';

class BeaconsMockClientV1 implements IBeaconsClientV1 {
  final int _maxPageSize = 100;
  List<BeaconV1> _items = <BeaconV1>[];

  BeaconsMockClientV1([List<BeaconV1> items]) {
    if (items != null) _items = json.decode(json.encode(items));
  }

  Function compose_filter(FilterParams filter) {
    filter = filter ?? FilterParams();

    var id = filter.getAsNullableString('id');
    var siteId = filter.getAsNullableString('site_id');
    var label = filter.getAsNullableString('label');
    var udi = filter.getAsNullableString('udi');
    var udis = filter.getAsObject('udis');

    if (udis is String) udis = udis.split(',');
    if (udis is! List) udis = null;

    return (item) {
      if (id != null && item.id != id) return false;
      if (siteId != null && item.site_id != siteId) return false;
      if (label != null && item.label != label) return false;
      if (udi != null && item.udi != udi) return false;
      if (udis != null && udis.indexOf(item.udi) < 0) return false;
      return true;
    };
  }

  @override
  Future<DataPage<BeaconV1>> getBeacons(
      String correlationId, FilterParams filter, PagingParams paging) {
    var filterBeacos = compose_filter(filter);
    var beacons = _items.where((el) => filterBeacos(el)).toList();

    // Extract a page
    paging = paging ?? PagingParams();
    var skip = paging.getSkip(-1);
    var take = paging.getTake(_maxPageSize);
    var total;

    if (paging.total) total = beacons.length;
    if (skip > 0) beacons = beacons.sublist(skip);

    beacons = beacons.take(take).toList();
    var page = DataPage<BeaconV1>(beacons, total);

    return Future<DataPage<BeaconV1>>.value(page);
  }

  @override
  Future<BeaconV1> getBeaconById(String correlationId, String beaconId) {
    var beacons = _items.where((x) => x.id == beaconId).toList();
    var beacon = beacons.isNotEmpty ? beacons[0] : null;
    return Future<BeaconV1>.value(beacon);
  }

  @override
  Future<BeaconV1> getBeaconByUdi(String correlationId, String udi) {
    var beacons = _items.where((x) => x.udi == udi).toList();
    var beacon = beacons.isNotEmpty ? beacons[0] : null;
    return Future<BeaconV1>.value(beacon);
  }

  @override
  Future<Map<String, dynamic>> calculatePosition(
      String correlationId, String siteId, List<String> udis) async {
    List<BeaconV1> beacons;
    Map<String, dynamic> position;

    if (udis == null || udis.isEmpty) return null;

    var page = await getBeacons(correlationId,
        FilterParams.fromTuples(['site_id', siteId, 'udis', udis]), null);
    beacons = page != null ? page.data : [];

    var lat = 0.0;
    var lng = 0.0;
    var count = 0;

    for (var beacon in beacons) {
      if (beacon.center != null &&
          beacon.center['type'] == 'Point' &&
          beacon.center['coordinates'] is List) {
        lng += beacon.center['coordinates'][0];
        lat += beacon.center['coordinates'][1];
        count += 1;
      }
      if (count > 0) {
        position = {
          'type': 'Point',
          'coordinates': [lng / count, lat / count],
        };
      }
    }

    return Future<Map<String, dynamic>>.value(position);
  }

  @override
  Future<BeaconV1> createBeacon(String correlationId, BeaconV1 beacon) {
    if (beacon == null) return null;
    beacon = beacon.clone();

    beacon.id = beacon.id ?? IdGenerator.nextLong();

    _items.add(beacon);
    return Future<BeaconV1>.value(beacon);
  }

  @override
  Future<BeaconV1> updateBeacon(String correlationId, BeaconV1 beacon) {
    var index = _items.map((x) => x.id).toList().indexOf(beacon.id);
    if (index < 0) return null;

    beacon = beacon.clone();
    _items[index] = beacon;
    return Future<BeaconV1>.value(beacon);
  }

  @override
  Future<BeaconV1> deleteBeaconById(String correlationId, String beaconId) {
    var index = _items.map((x) => x.id).toList().indexOf(beaconId);
    var item = _items[index];
    if (index < 0) return null;
    _items.removeAt(index);
    return Future<BeaconV1>.value(item);
  }
}

/src/version1/BeaconsMockClientV1.py


import copy
from typing import List, Any, Optional

from pip_services3_commons.data import FilterParams, IdGenerator, PagingParams, DataPage

from src.clients.version1.IBeaconsClientV1 import IBeaconsClientV1
from src.data.version1 import BeaconV1

filtered = filter


class BeaconsMockClientV1(IBeaconsClientV1):
    _max_page_size = 100
    _items: List[BeaconV1] = []

    def compose_filter(self, filter: FilterParams) -> Any:
        filter = filter or FilterParams()
        id = filter.get_as_nullable_string("id")
        site_id = filter.get_as_nullable_string("site_id")
        label = filter.get_as_nullable_string('label')
        udi = filter.get_as_nullable_string('udi')
        udis = filter.get_as_object('udis')

        if type(udis) == str:
            udis = udis.split(',')

        if not (type(udis) == list):
            udis = None

        def filter_beacons(item):
            if id is not None and item['id'] != id:
                return False
            if site_id is not None and item['site_id'] != site_id:
                return False
            if label is not None and item['label'] != label:
                return False
            if udi is not None and item['udi'] != udi:
                return False
            if udis is not None and item['udi'] not in udis:
                try:
                    udis.index(item.udi)
                except Exception as e:
                    return False
            return True

        return filter_beacons

    def get_beacons(self, correlation_id: Optional[str], filter: FilterParams, paging: PagingParams) -> DataPage:
        filter_beacons = self.compose_filter(filter)
        beacons = [item for item in self._items if filter_beacons(item) is True]

        # Extract a page
        paging = paging if paging is not None else PagingParams()
        skip = paging.get_skip(-1)
        take = paging.get_take(self._max_page_size)
        total = None
        if paging.total:
            total = len(beacons)
        if skip > 0:
            beacons = beacons[skip:]
            beacons = beacons[:take]

        page = DataPage(beacons, total)
        return page

    def get_beacon_by_id(self, correlation_id: Optional[str], id: str) -> dict:
        beacons = [item for item in self._items if item['id'] == id]
        beacon = beacons[0] if len(beacons) > 0 else None
        return beacon

    def get_beacon_by_udi(self, correlation_id: Optional[str], udi: str) -> dict:
        beacons = [item for item in self._items if item['udi'] == udi]
        beacon = beacons[0] if len(beacons) > 0 else None
        return beacon

    def get_beacons_by_filter(self, correlation_id: Optional[str], filter: FilterParams, paging: PagingParams,
                              sort=None, select=None):
        items = list(self._items)

        # Filter and sort
        if filter is not None:
            items = list(filtered(filter, items))
        if sort is not None:
            items = list(items.sort(key=sort))
            # items = sorted(items, sort)

        # Prepare paging parameters
        paging = paging if not (paging is None) else PagingParams()
        skip = paging.get_skip(-1)
        take = paging.get_take(self._max_page_size)

        # Get a page
        data = items
        if skip > 0:
            data = data[skip:]
        if take > 0:
            data = data[:take + 1]

        # Convert values
        if not (select is None):
            data = map(select, data)

        # Return a page
        return DataPage(data, len(items))

    def calculate_position(self, correlation_id: Optional[str], site_id: str, udis: List[str]) -> Any:
        beacons: List[BeaconV1]
        position = None

        if udis is None or len(udis) == 0:
            return

        page = self.get_beacons(correlation_id, FilterParams.from_tuples(
            'site_id', site_id,
            'udis', udis
        ), None)
        beacons = page.data if page.data else []

        lat = 0
        lng = 0
        count = 0

        for beacon in beacons:
            if beacon['center'] is not None and beacon['center']['type'] == "Point" and len(
                    beacon['center']['coordinates']) > 1:
                lng = lng + beacon['center']['coordinates'][0]
                lat = lat + beacon['center']['coordinates'][1]
                count = count + 1

        if count > 0:
            position = {"type": 'Point', "coordinates": [lng / count, lat / count]}
            return position
        return None

    def create_beacon(self, correlation_id: Optional[str], beacon: BeaconV1) -> dict:
        if beacon is None:
            return

        beacon = copy.deepcopy(beacon)
        beacon.id = beacon.id or IdGenerator.next_long()
        self._items.append(beacon)
        return beacon

    def update_beacon(self, correlation_id: Optional[str], beacon: BeaconV1) -> dict:
        try:
            index = list(map(lambda x: x.id, self._items)).index(beacon['id'])
        except ValueError:
            return

        beacon = copy.deepcopy(beacon)
        self._items[index] = beacon
        return beacon

    def delete_beacon_by_id(self, correlation_id: Optional[str], beacon_id: str) -> dict:
        try:
            index = list(map(lambda x: x.id, self._items)).index(beacon_id)
        except ValueError:
            return

        beacon = self._items[index]
        del self._items[index]

        return beacon
Not available

Now let’s test the client we’ve created. We’ll be using the set of tests that we developed in one of the previous steps, and adding just one test file that will bring it all together. The source of this file is presented below:

/test/version1/test_BeaconsMockClientV1.py

/test/version1/BeaconsMockClient.test.ts


import { BeaconsMockClientV1 } from '../../src/version1/BeaconsMockClientV1';
import { BeaconsClientV1Fixture } from './BeaconsClientV1Fixture';

suite("BeaconsMockClientV1", () => {
    let client: BeaconsMockClientV1;
    let fixture: BeaconsClientV1Fixture;

    setup(() => {
        client = new BeaconsMockClientV1();
        fixture = new BeaconsClientV1Fixture(client);
    });
    
    teardown(() => {
    });

    test("CRUD Operations", async () => {
        await fixture.testCrudOperations();
    });
});


/test/version1/test_BeaconsMockClientV1.cs

    [Collection("Sequential")]
    public class BeaconsMockClientV1Test
    {
        private BeaconsMockClientV1 _client;
        private BeaconsClientV1Fixture _fixture;

        public BeaconsMockClientV1Test()
        {
            _client = new BeaconsMockClientV1();
            _fixture = new BeaconsClientV1Fixture(_client);
        }

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

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


/test/version1/BeaconsMemoryClientV1_test.go

package test_clients1

import (
	"testing"

	clients1 "github.com/pip-services-samples/client-beacons-gox/clients/version1"
)

type beaconsMemoryClientV1Test struct {
	client  *clients1.BeaconsMemoryClientV1
	fixture *BeaconsClientV1Fixture
}

func newBeaconsMemoryClientV1Test() *beaconsMemoryClientV1Test {

	return &beaconsMemoryClientV1Test{}
}

func (c *beaconsMemoryClientV1Test) setup(t *testing.T) {
	c.client = clients1.NewBeaconsMemoryClientV1(nil)
	c.fixture = NewBeaconsClientV1Fixture(c.client)
}

func (c *beaconsMemoryClientV1Test) teardown(t *testing.T) {
	c.client = nil
	c.fixture = nil
}

func TestBeaconsMemoryClientV1(t *testing.T) {
	c := newBeaconsMemoryClientV1Test()

	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/BeaconsMockClientV1_test.dart

import 'package:test/test.dart';
import 'package:pip_services_beacons_dart/pip_services_beacons_dart.dart';
import './BeaconsClientV1Fixture.dart';

void main() {
  group('BeaconsMockClientV1', () {
    BeaconsMockClientV1 client;
    BeaconsClientV1Fixture fixture;

    setUp(() async {
      client = BeaconsMockClientV1();
      fixture = BeaconsClientV1Fixture(client);
    });

    tearDown(() async {});

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

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

/test/version1/test_BeaconsMockClientV1.py

from src.clients.version1.BeaconsMockClientV1 import BeaconsMockClientV1

from test.clients.version1.BeaconsClientV1Fixture import BeaconsClientV1Fixture


class TestBeaconsMockClientV1:
    client: BeaconsMockClientV1
    fixture: BeaconsClientV1Fixture

    @classmethod
    def setup_class(cls):
        cls.client = BeaconsMockClientV1()
        cls.fixture = BeaconsClientV1Fixture(cls.client)

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

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


Not available

Create a file with the tests and run them. All the tests should pass, even though the server-side code wasn’t actually used anywhere.

This technique becomes very useful when developing microservices that bring together multiple microservices by means of their clients (e.g. a facade microservice). It allows us to perform functional testing without having to run the entire infrastructure.

To performing non-fuctional testing, we need to generate a large amount of realistic data. Users usually don’t know the entire data structure with all of its variations and exceptions. The next component we will be adding to our client library is a random data generator. This component can be used by the microservice’s users to create quality tests. The implementation is usually done in the form of static methods that either return an entire object, or just some part of its parameters. Let’s take a look at what an implementation of such a generator for the BeaconsV1 data object would look like. The generator’s code is listed below:

import { RandomArray, RandomInteger } from 'pip-services3-commons-node';
import { BeaconV1 } from './BeaconV1'
import { BeaconTypeV1 } from './BeaconTypeV1'

export class RandomBeaconV1 {
    public static nextBeaconType(): string {
        return RandomArray.pick([BeaconTypeV1.AltBeacon, BeaconTypeV1.EddyStoneUdi, BeaconTypeV1.Unknown, BeaconTypeV1.iBeacon]);
    }

    public static nextBeaconCenter(): any {
        return {
            type: 'Point',
            center: {
                coordinates: [RandomInteger.nextInteger(1, 1000), RandomInteger.nextInteger(1, 1000)]
            }
        }
    }

    public static nextBeacon(): BeaconV1 {
        var beacon = new BeaconV1();
        beacon.type = RandomBeaconV1.nextBeaconType();
        beacon.radius = RandomInteger.nextInteger(1, 1000);
        beacon.udi = RandomArray.pick(['00001', '00002', '00003', '00004']);
        beacon.center = RandomBeaconV1.nextBeaconCenter();
        return beacon;
    }

}
public class RandomBeaconV1
{
	public static BeaconV1 NextBeacon(int siteCount = 100)
	{
        return new BeaconV1()
        {
            Id = IdGenerator.NextLong(),
            SiteId = NextSiteId(siteCount),
            Udi = IdGenerator.NextShort(),
            Label = RandomString.NextString(10, 25),
            Type = NextBeaconType(),
            Radius = RandomFloat.NextFloat(3, 150),
            Center = NextPosition()
        };
    }

    public static string NextSiteId(int siteCount = 100)
    {
        return RandomInteger.NextInteger(1, siteCount).ToString();
    }

    public static string NextBeaconType()
    {
        var choice = RandomInteger.NextInteger(0, 3);
        switch (choice)
        {
            case 0:
                return BeaconTypeV1.iBeacon;
            case 1:
                return BeaconTypeV1.AltBeacon;
            case 2:
                return BeaconTypeV1.EddyStoneUdi;
            case 3:
                return BeaconTypeV1.Unknown;
            default:
                return BeaconTypeV1.Unknown;
        }
    }

    public static CenterObjectV1 NextPosition()
    {
        return new CenterObjectV1
        {
            Type = "Point",
            Coordinates = new double[]
            {
                RandomFloat.NextFloat(-180, 168), // Longitude
                RandomFloat.NextFloat(-90, 90), // Latitude
            }
        };
    }
}

package data1

type BeaconV1 struct {
	Id     string     `json:"id" bson:"_id"`
	SiteId string     `json:"site_id" bson:"site_id"`
	Type   string     `json:"type" bson:"type"`
	Udi    string     `json:"udi" bson:"udi"`
	Label  string     `json:"label" bson:"label"`
	Center GeoPointV1 `json:"center" bson:"center"` // GeoJson
	Radius float32    `json:"radius" bson:"radius"`
}

func (b BeaconV1) Clone() BeaconV1 {
	return BeaconV1{
		Id:     b.Id,
		SiteId: b.SiteId,
		Type:   b.Type,
		Udi:    b.Udi,
		Label:  b.Label,
		Center: b.Center.Clone(),
		Radius: b.Radius,
	}
}

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

class RandomBeaconV1 {
  static String nextBeaconType() {
    return RandomArray.pick([
      BeaconTypeV1.altBeacon,
      BeaconTypeV1.eddyStoneUdi,
      BeaconTypeV1.unknown,
      BeaconTypeV1.iBeacon
    ]);
  }

  static Map<String, dynamic> nextBeaconCenter() {
    return {
      'type': 'Point',
      'center': {
        'coordinates': [
          RandomInteger.nextInteger(1, 1000),
          RandomInteger.nextInteger(1, 1000)
        ]
      }
    };
  }

  static BeaconV1 nextBeacon() {
    var beacon = BeaconV1();
    beacon.type = RandomBeaconV1.nextBeaconType();
    beacon.radius = RandomDouble.nextDouble(1, 1000);
    beacon.udi = RandomArray.pick(['00001', '00002', '00003', '00004']);
    beacon.center = RandomBeaconV1.nextBeaconCenter();
    return beacon;
  }
}

# -*- coding: utf-8 -*-
from pip_services3_commons.random import RandomArray, RandomInteger

from src.data.version1 import BeaconTypeV1, BeaconV1


class RandomBeaconV1:
    @staticmethod
    def next_beacon_type() -> str:
        return RandomArray.pick(
            [
                BeaconTypeV1.AltBeacon, BeaconTypeV1.EddyStoneUdi,
                BeaconTypeV1.Unknown, BeaconTypeV1.iBeacon
            ]
        )

    @staticmethod
    def next_beacon() -> BeaconV1:
        return BeaconV1(
            id=None,
            site_id=None,
            type=RandomBeaconV1.next_beacon_type(),
            udi=RandomArray.pick(['00001', '00002', '00003', '00004']),
            label=None,
            center=RandomBeaconV1.next_beacon_type(),
            radius=RandomInteger.next_integer(1, 1000)
        )

Not available

In this implementation, the ranges of generated values are statically set, but they can be passed as parameters to the methods and dynamically set as needed. Using this instrument, we can easily generate large volumes of realistic data. This, in turn, can be used to test, for example, how fast the system can create elements in the persistence it’s using.

In the Step 6. Testing the Client with a Remote Microservice, we’ll be taking a look at how to test our client using a microservice that is remotely deployed in a Docker container.

Step 6. Testing the Client with a Remote Microservice