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
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()
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)
)
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.