diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d627359e93ea57b865c91f211f5683ee7b5a8a07..f72a4a8aa62369b978523a429881d679815955cd 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,10 +1,10 @@
 # stages of the cicd pipeline
 stages:
+  - dependencies
   - build
   - test
   - unit_test
   - integ_test
-  - dependencies
   - deploy
   - funct_test
 
diff --git a/run_local_tests.sh b/run_local_tests.sh
index c817a792ddfbb203cb72d4aa9dcc1cbd56456196..71ff06a54638f1392b07dac9c4d1323e3a2ec3da 100755
--- a/run_local_tests.sh
+++ b/run_local_tests.sh
@@ -17,7 +17,8 @@ coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \
     context/tests/test_unitary.py
 
 coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \
-    device/tests/test_unitary.py
+    device/tests/test_unitary_driverapi.py \
+    device/tests/test_unitary_service.py
 
 coverage run --rcfile=$RCFILE --append -m pytest --log-level=INFO --verbose \
     service/tests/test_unitary.py
diff --git a/src/common/Checkers.py b/src/common/Checkers.py
index 4ac047a1c6d9078b66d70e6a97755cd3ac6349cf..b9e0f3067357912ef8e7404768b234f3c58d40e5 100644
--- a/src/common/Checkers.py
+++ b/src/common/Checkers.py
@@ -1,4 +1,4 @@
-from typing import Any, Set, Union
+from typing import Any, List, Set, Sized, Tuple, Union
 
 def chk_none(name : str, value : Any) -> Any:
     if value is not None:
@@ -18,13 +18,60 @@ def chk_type(name : str, value : Any, type_or_types : Union[type, Set[type]] = s
         raise AttributeError(msg.format(str(name), str(value), type(value).__name__, str(type_or_types)))
     return value
 
-def chk_string(name, value, allow_empty=False) -> str:
-    chk_not_none(name, value)
+def chk_length(
+    name : str, value : Sized, allow_empty : bool = False,
+    min_length : Union[int, None] = None, max_length : Union[int, None] = None,
+    allowed_lengths : Union[None, int, Set[int], List[int], Tuple[int]] = None) -> Any:
+
+    length = len(chk_type(name, value, Sized))
+
+    allow_empty = chk_type('allow_empty for {}'.format(name), allow_empty, bool)
+    if not allow_empty and length == 0:
+        msg = '{}({}) is out of range: allow_empty({}) min_length({}) max_length({}) allowed_lengths({}).'
+        raise AttributeError(msg.format(
+            str(name), str(value), str(allow_empty), str(min_length), str(max_length), str(allowed_lengths)))
+
+
+    if min_length is not None:
+        min_length = chk_type('min_length for {}'.format(name), min_length, int)
+        if length < min_length:
+            msg = '{}({}) is out of range: allow_empty({}) min_length({}) max_length({}) allowed_lengths({}).'
+            raise AttributeError(msg.format(
+                str(name), str(value), str(allow_empty), str(min_length), str(max_length), str(allowed_lengths)))
+
+    if max_length is not None:
+        max_length = chk_type('max_length for {}'.format(name), max_length, int)
+        if length > max_length:
+            msg = '{}({}) is out of range: allow_empty({}) min_value({}) max_value({}) allowed_lengths({}).'
+            raise AttributeError(msg.format(
+                str(name), str(value), str(allow_empty), str(max_length), str(max_length), str(allowed_lengths)))
+
+    if allowed_lengths is not None:
+        chk_type('allowed_lengths for {}'.format(name), allowed_lengths, (int, set, list, tuple))
+        if isinstance(allowed_lengths, int):
+            fixed_length = allowed_lengths
+            if length != fixed_length:
+                msg = '{}({}) is out of range: allow_empty({}) min_value({}) max_value({}) allowed_lengths({}).'
+                raise AttributeError(msg.format(
+                    str(name), str(value), str(allow_empty), str(max_length), str(max_length), str(allowed_lengths)))
+        else:
+            for i in allowed_lengths: chk_type('allowed_lengths[#{}] for {}'.format(i, name), i, int)
+            if length not in allowed_lengths:
+                msg = '{}({}) is out of range: allow_empty({}) min_value({}) max_value({}) allowed_lengths({}).'
+                raise AttributeError(msg.format(
+                    str(name), str(value), str(allow_empty), str(max_length), str(max_length), str(allowed_lengths)))
+
+    return value
+
+def chk_string(
+    name : str, value : Any, allow_empty : bool = False,
+    min_length : Union[int, None] = None, max_length : Union[int, None] = None,
+    allowed_lengths : Union[None, int, Set[int], List[int], Tuple[int]] = None) -> str:
+
     chk_type(name, value, str)
-    if allow_empty: return value
-    if len(value) == 0:
-        msg = '{}({}) string is empty.'
-        raise AttributeError(msg.format(str(name), str(value)))
+    chk_length(
+        name, value, allow_empty=allow_empty, min_length=min_length, max_length=max_length,
+        allowed_lengths=allowed_lengths)
     return value
 
 def chk_float(name, value, type_or_types=(int, float), min_value=None, max_value=None) -> float:
diff --git a/src/context/tests/test_unitary.py b/src/context/tests/test_unitary.py
index b3a22c60d1aad2e7ad70db42ecfd1d0641a5749a..bba2a346adc2554e81b9048394fb964c4144a982 100644
--- a/src/context/tests/test_unitary.py
+++ b/src/context/tests/test_unitary.py
@@ -88,17 +88,16 @@ def test_delete_link_empty_uuid(context_client : ContextClient):
         copy_link_id['link_id']['uuid'] = ''
         context_client.DeleteLink(LinkId(**copy_link_id))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'link_id.link_id.uuid() string is empty.'
+    msg = 'link_id.link_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
 def test_add_link_already_exists(context_client : ContextClient):
     # should fail with link already exists
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
         context_client.AddLink(Link(**LINK))
     assert e.value.code() == grpc.StatusCode.ALREADY_EXISTS
-    msg = ' '.join([
-        'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)',
-        'already exists in the database.',
-    ])
+    msg = 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) already exists in the database.'
     assert e.value.details() == msg
 
 def test_delete_link(context_client : ContextClient):
@@ -113,10 +112,7 @@ def test_delete_link_not_existing(context_client : ContextClient):
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
         context_client.DeleteLink(LinkId(**LINK_ID))
     assert e.value.code() == grpc.StatusCode.NOT_FOUND
-    msg = ' '.join([
-        'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)',
-        'does not exist in the database.'
-    ])
+    msg = 'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) does not exist in the database.'
     assert e.value.details() == msg
 
 def test_add_link_uuid_empty(context_client : ContextClient):
@@ -126,7 +122,9 @@ def test_add_link_uuid_empty(context_client : ContextClient):
         copy_link['link_id']['link_id']['uuid'] = ''
         context_client.AddLink(Link(**copy_link))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'link.link_id.link_id.uuid() string is empty.'
+    msg = 'link.link_id.link_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
 def test_add_link_wrong_endpoint(context_client : ContextClient):
     # should fail with wrong endpoint context
@@ -135,11 +133,9 @@ def test_add_link_wrong_endpoint(context_client : ContextClient):
         copy_link['endpointList'][0]['topoId']['contextId']['contextUuid']['uuid'] = 'wrong-context'
         context_client.AddLink(Link(**copy_link))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Context(wrong-context) in Endpoint(#0) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)',
-        'mismatches acceptable Contexts({\'admin\'}).',
-        'Optionally, leave field empty to use predefined Context(admin).',
-    ])
+    msg = 'Context(wrong-context) in Endpoint(#0) of '\
+          'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) mismatches acceptable Contexts({\'admin\'}). '\
+          'Optionally, leave field empty to use predefined Context(admin).'
     assert e.value.details() == msg
 
     # should fail with wrong endpoint topology
@@ -148,12 +144,9 @@ def test_add_link_wrong_endpoint(context_client : ContextClient):
         copy_link['endpointList'][0]['topoId']['topoId']['uuid'] = 'wrong-topo'
         context_client.AddLink(Link(**copy_link))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Context(admin)/Topology(wrong-topo)',
-        'in Endpoint(#0) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)',
-        'mismatches acceptable Topologies({\'admin\'}).',
-        'Optionally, leave field empty to use predefined Topology(admin).',
-    ])
+    msg = 'Context(admin)/Topology(wrong-topo) in Endpoint(#0) of '\
+          'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) mismatches acceptable Topologies({\'admin\'}). '\
+          'Optionally, leave field empty to use predefined Topology(admin).'
     assert e.value.details() == msg
 
     # should fail with device uuid is empty
@@ -162,7 +155,9 @@ def test_add_link_wrong_endpoint(context_client : ContextClient):
         copy_link['endpointList'][0]['dev_id']['device_id']['uuid'] = ''
         context_client.AddLink(Link(**copy_link))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'endpoint_id[#0].dev_id.device_id.uuid() string is empty.'
+    msg = 'endpoint_id[#0].dev_id.device_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
     # should fail with wrong endpoint device
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
@@ -170,11 +165,8 @@ def test_add_link_wrong_endpoint(context_client : ContextClient):
         copy_link['endpointList'][0]['dev_id']['device_id']['uuid'] = 'wrong-device'
         context_client.AddLink(Link(**copy_link))
     assert e.value.code() == grpc.StatusCode.NOT_FOUND
-    msg = ' '.join([
-        'Context(admin)/Topology(admin)/Device(wrong-device)',
-        'in Endpoint(#0) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)',
-        'does not exist in the database.',
-    ])
+    msg = 'Context(admin)/Topology(admin)/Device(wrong-device) in Endpoint(#0) of '\
+          'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) does not exist in the database.'
     assert e.value.details() == msg
 
     # should fail with endpoint uuid is empty
@@ -183,7 +175,9 @@ def test_add_link_wrong_endpoint(context_client : ContextClient):
         copy_link['endpointList'][0]['port_id']['uuid'] = ''
         context_client.AddLink(Link(**copy_link))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'endpoint_id[#0].port_id.uuid() string is empty.'
+    msg = 'endpoint_id[#0].port_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
     # should fail with wrong endpoint port
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
@@ -191,11 +185,8 @@ def test_add_link_wrong_endpoint(context_client : ContextClient):
         copy_link['endpointList'][0]['port_id']['uuid'] = 'wrong-port'
         context_client.AddLink(Link(**copy_link))
     assert e.value.code() == grpc.StatusCode.NOT_FOUND
-    msg = ' '.join([
-        'Context(admin)/Topology(admin)/Device(DEV1)/Port(wrong-port)',
-        'in Endpoint(#0) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1)',
-        'does not exist in the database.',
-    ])
+    msg = 'Context(admin)/Topology(admin)/Device(DEV1)/Port(wrong-port) in Endpoint(#0) of '\
+          'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1) does not exist in the database.'
     assert e.value.details() == msg
 
     # should fail with endpoint device duplicated
@@ -204,10 +195,8 @@ def test_add_link_wrong_endpoint(context_client : ContextClient):
         copy_link['endpointList'][1]['dev_id']['device_id']['uuid'] = 'DEV1'
         context_client.AddLink(Link(**copy_link))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Duplicated Context(admin)/Topology(admin)/Device(DEV1)',
-        'in Endpoint(#1) of Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1).',
-    ])
+    msg = 'Duplicated Context(admin)/Topology(admin)/Device(DEV1) in Endpoint(#1) of '\
+          'Context(admin)/Topology(admin)/Link(DEV1/EP2 ==> DEV2/EP1).'
     assert e.value.details() == msg
 
 def test_add_link(context_client : ContextClient):
diff --git a/src/device/.gitlab-ci.yml b/src/device/.gitlab-ci.yml
index 0d538ad47c4e7b95c7e012ef0750039c89ac4a06..fca94bf712606f590c13c3e90f8fc686b9238471 100644
--- a/src/device/.gitlab-ci.yml
+++ b/src/device/.gitlab-ci.yml
@@ -35,7 +35,7 @@ unit_test device:
     - sleep 5
     - docker ps -a
     - docker logs $IMAGE_NAME
-    - docker exec -i $IMAGE_NAME bash -c "pytest --log-level=DEBUG --verbose $IMAGE_NAME/tests/test_unitary.py"
+    - docker exec -i $IMAGE_NAME bash -c "pytest --log-level=DEBUG --verbose $IMAGE_NAME/tests/test_unitary_service.py $IMAGE_NAME/tests/test_unitary_driverapi.py"
   after_script:
     - docker stop $IMAGE_NAME
     - docker rm $IMAGE_NAME
diff --git a/src/device/requirements.in b/src/device/requirements.in
index 25abdad1b5767117956a88b816399635348884c7..5a05d7efa6ef6964717635123756da0aead4ceff 100644
--- a/src/device/requirements.in
+++ b/src/device/requirements.in
@@ -1,6 +1,10 @@
+anytree
+apscheduler
+fastcache
 grpcio-health-checking
 grpcio
 prometheus-client
 pytest
 pytest-benchmark
+pytz
 redis
diff --git a/src/device/service/DeviceServiceServicerImpl.py b/src/device/service/DeviceServiceServicerImpl.py
index 84ff22b3cad32252160596a457ea8358bcad953b..c5893b7387b46d06262a808aea09e870178e7648 100644
--- a/src/device/service/DeviceServiceServicerImpl.py
+++ b/src/device/service/DeviceServiceServicerImpl.py
@@ -1,11 +1,8 @@
-from typing import List, Tuple
 import grpc, logging
 from prometheus_client import Counter, Histogram
-from common.Checkers import chk_options, chk_string
 from common.database.api.context.Constants import DEFAULT_CONTEXT_ID, DEFAULT_TOPOLOGY_ID
 from common.database.api.Database import Database
-from common.database.api.context.topology.device.OperationalStatus import OperationalStatus, \
-    operationalstatus_enum_values, to_operationalstatus_enum
+from common.database.api.context.topology.device.OperationalStatus import OperationalStatus
 from common.exceptions.ServiceException import ServiceException
 from device.proto.context_pb2 import DeviceId, Device, Empty
 from device.proto.device_pb2_grpc import DeviceServiceServicer
diff --git a/src/device/service/driver_api/DriverFactory.py b/src/device/service/driver_api/DriverFactory.py
new file mode 100644
index 0000000000000000000000000000000000000000..c226e434c75286d99c2051098e0d18d9cf483caa
--- /dev/null
+++ b/src/device/service/driver_api/DriverFactory.py
@@ -0,0 +1,50 @@
+from typing import Dict, Set
+from device.service.driver_api.QueryFields import QUERY_FIELDS
+from device.service.driver_api._Driver import _Driver
+from device.service.driver_api.Exceptions import MultipleResultsForQueryException, UnsatisfiedQueryException, \
+    UnsupportedDriverClassException, UnsupportedQueryFieldException, UnsupportedQueryFieldValueException
+
+class DriverFactory:
+    def __init__(self) -> None:
+        self.__indices : Dict[str, Dict[str, Set[_Driver]]] = {} # Dict{field_name => Dict{field_value => Set{Driver}}}
+
+    def register_driver_class(self, driver_class, **query_fields):
+        if not issubclass(driver_class, _Driver): raise UnsupportedDriverClassException(str(driver_class))
+
+        driver_name = driver_class.__name__
+        unsupported_query_fields = set(query_fields.keys()).difference(set(QUERY_FIELDS.keys()))
+        if len(unsupported_query_fields) > 0:
+            raise UnsupportedQueryFieldException(unsupported_query_fields, driver_class_name=driver_name)
+
+        for field_name, field_value in query_fields.items():
+            field_indice = self.__indices.setdefault(field_name, dict())
+            field_enum_values = QUERY_FIELDS.get(field_name)
+            if field_enum_values is not None and field_value not in field_enum_values:
+                raise UnsupportedQueryFieldValueException(
+                    field_name, field_value, field_enum_values, driver_class_name=driver_name)
+            field_indice_drivers = field_indice.setdefault(field_name, set())
+            field_indice_drivers.add(driver_class)
+
+    def get_driver_class(self, **query_fields) -> _Driver:
+        unsupported_query_fields = set(query_fields.keys()).difference(set(QUERY_FIELDS.keys()))
+        if len(unsupported_query_fields) > 0: raise UnsupportedQueryFieldException(unsupported_query_fields)
+
+        candidate_driver_classes = None
+
+        for field_name, field_value in query_fields.items():
+            field_indice = self.__indices.get(field_name)
+            if field_indice is None: continue
+            field_enum_values = QUERY_FIELDS.get(field_name)
+            if field_enum_values is not None and field_value not in field_enum_values:
+                raise UnsupportedQueryFieldValueException(field_name, field_value, field_enum_values)
+            field_indice_drivers = field_indice.get(field_name)
+            if field_indice_drivers is None: continue
+
+            candidate_driver_classes = set().union(field_indice_drivers) if candidate_driver_classes is None else \
+                candidate_driver_classes.intersection(field_indice_drivers)
+
+        if len(candidate_driver_classes) == 0: raise UnsatisfiedQueryException(query_fields)
+        if len(candidate_driver_classes) >  1:
+            # TODO: Consider choosing driver with more query fields being satisfied (i.e., the most restrictive one)
+            raise MultipleResultsForQueryException(query_fields, {d.__name__ for d in candidate_driver_classes})
+        return candidate_driver_classes.pop()
diff --git a/src/device/service/driver_api/Exceptions.py b/src/device/service/driver_api/Exceptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..2275bb04cbf9334a6823544bc48e8ad345d2dc20
--- /dev/null
+++ b/src/device/service/driver_api/Exceptions.py
@@ -0,0 +1,30 @@
+class MultipleResultsForQueryException(Exception):
+    def __init__(self, query_fields, driver_names):
+        super().__init__('Multiple Drivers({}) satisfy QueryFields({})'.format(str(driver_names), str(query_fields)))
+
+class UnsatisfiedQueryException(Exception):
+    def __init__(self, query_fields):
+        super().__init__('No Driver satisfies QueryFields({})'.format(str(query_fields)))
+
+class UnsupportedDriverClassException(Exception):
+    def __init__(self, driver_class_name):
+        super().__init__('Class({}) is not a subclass of _Driver'.format(str(driver_class_name)))
+
+class UnsupportedQueryFieldException(Exception):
+    def __init__(self, unsupported_query_fields, driver_class_name=None):
+        if driver_class_name:
+            msg = 'QueryFields({}) specified by Driver({}) are not supported'.format(
+                str(unsupported_query_fields), str(driver_class_name))
+        else:
+            msg = 'QueryFields({}) specified in query are not supported'.format(str(unsupported_query_fields))
+        super().__init__(msg)
+
+class UnsupportedQueryFieldValueException(Exception):
+    def __init__(self, query_field_name, query_field_value, allowed_query_field_values, driver_class_name=None):
+        if driver_class_name:
+            msg = 'QueryField({}={}) specified by Driver({}) is not supported. Allowed values are {}'.format(
+                str(query_field_name), str(query_field_value), str(driver_class_name), str(allowed_query_field_values))
+        else:
+            msg = 'QueryField({}={}) specified in query is not supported. Allowed values are {}'.format(
+                str(query_field_name), str(query_field_value), str(allowed_query_field_values))
+        super().__init__(msg)
diff --git a/src/device/service/driver_api/QueryFields.py b/src/device/service/driver_api/QueryFields.py
new file mode 100644
index 0000000000000000000000000000000000000000..15b3f5b7582283083c9ea1080dc4f3d6f5390501
--- /dev/null
+++ b/src/device/service/driver_api/QueryFields.py
@@ -0,0 +1,33 @@
+from enum import Enum
+
+class DeviceTypeQueryFieldEnum(Enum):
+    OPTICAL_ROADM       = 'optical-roadm'
+    OPTICAL_TRANDPONDER = 'optical-trandponder'
+    PACKET_ROUTER       = 'packet-router'
+    PACKET_SWITCH       = 'packet-switch'
+
+class ProtocolQueryFieldEnum(Enum):
+    SOFTWARE = 'software'
+    GRPC     = 'grpc'
+    RESTAPI  = 'restapi'
+    NETCONF  = 'netconf'
+    GNMI     = 'gnmi'
+    RESTCONF = 'restconf'
+
+class DataModelQueryFieldEnum(Enum):
+    EMULATED              = 'emu'
+    OPENCONFIG            = 'oc'
+    P4                    = 'p4'
+    TRANSPORT_API         = 'tapi'
+    IETF_NETWORK_TOPOLOGY = 'ietf-netw-topo'
+    ONF_TR_352            = 'onf-tr-352'
+
+# Map allowed query fields to allowed values per query field. If no restriction (free text) None is specified
+QUERY_FIELDS = {
+    'device_type'  : {i.value for i in DeviceTypeQueryFieldEnum},
+    'protocol'     : {i.value for i in ProtocolQueryFieldEnum},
+    'data_model'   : {i.value for i in DataModelQueryFieldEnum},
+    'vendor'       : None,
+    'model'        : None,
+    'serial_number': None,
+}
diff --git a/src/device/service/driver_api/_Driver.py b/src/device/service/driver_api/_Driver.py
new file mode 100644
index 0000000000000000000000000000000000000000..69ac93cb6a071a40aca25b28f930e390bfbfe6b0
--- /dev/null
+++ b/src/device/service/driver_api/_Driver.py
@@ -0,0 +1,138 @@
+from typing import Any, Iterator, List, Tuple, Union
+
+class _Driver:
+    def __init__(self, address : str, port : int, **kwargs) -> None:
+        """ Initialize Driver.
+            Parameters:
+                address : str
+                    The address of the device
+                port : int
+                    The port of the device
+                **kwargs
+                    Extra attributes can be configured using kwargs.
+        """
+        raise NotImplementedError()
+
+    def Connect(self) -> bool:
+        """ Connect to the Device.
+            Returns:
+                succeeded : bool
+                    Boolean variable indicating if connection succeeded.
+        """
+        raise NotImplementedError()
+
+    def Disconnect(self) -> bool:
+        """ Disconnect from the Device.
+            Returns:
+                succeeded : bool
+                    Boolean variable indicating if disconnection succeeded.
+        """
+        raise NotImplementedError()
+
+    def GetConfig(self, resource_keys : List[str]) -> List[Union[Any, None, Exception]]:
+        """ Retrieve running configuration of entire device, or selected resource keys.
+            Parameters:
+                resource_keys : List[str]
+                    List of keys pointing to the resources to be retrieved.
+            Returns:
+                values : List[Union[Any, None, Exception]]
+                    List of values for resource keys requested. Return values must be in the same order than resource
+                    keys requested. If a resource is found, the appropriate value type must be retrieved, if a resource
+                    is not found, None must be retrieved in the List for that resource. In case of Exception processing
+                    a resource, the Exception must be retrieved.
+        """
+        raise NotImplementedError()
+
+    def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
+        """ Create/Update configuration for a list of resources.
+            Parameters:
+                resources : List[Tuple[str, Any]]
+                    List of tuples, each containing a resource_key pointing the resource to be modified, and a
+                    resource_value containing the new value to be set.
+            Returns:
+                results : List[Union[bool, Exception]]
+                    List of results for resource key changes requested. Return values must be in the same order than
+                    resource keys requested. If a resource is properly set, True must be retrieved; otherwise, the
+                    Exception that is raised during the processing must be retrieved.
+        """
+        raise NotImplementedError()
+
+    def DeleteConfig(self, resource_keys : List[str]) -> List[Union[bool, Exception]]:
+        """ Delete configuration for a list of resource keys.
+            Parameters:
+                resource_keys : List[str]
+                    List of keys pointing to the resources to be deleted.
+            Returns:
+                results : List[bool]
+                    List of results for resource key deletions requested. Return values must be in the same order than
+                    resource keys requested. If a resource is properly deleted, True must be retrieved; otherwise, the
+                    Exception that is raised during the processing must be retrieved.
+        """
+        raise NotImplementedError()
+
+    def SubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]:
+        """ Subscribe to state information of entire device, or selected resources. Subscriptions are incremental.
+            Driver should keep track of requested resources.
+            Parameters:
+                subscriptions : List[Tuple[str, float, float]]
+                    List of tuples, each containing a resource_key pointing the resource to be subscribed, a
+                    sampling_duration, and a sampling_interval (both in seconds with float representation) defining,
+                    respectively, for how long monitoring should last, and the desired monitoring interval for the
+                    resource specified.
+            Returns:
+                results : List[bool]
+                    List of results for resource key subscriptions requested. Return values must be in the same order
+                    than resource keys requested. If a resource is properly subscribed, True must be retrieved;
+                    otherwise, the Exception that is raised during the processing must be retrieved.
+        """
+        raise NotImplementedError()
+
+    def UnsubscribeState(self, subscriptions : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]:
+        """ Unsubscribe from state information of entire device, or selected resources. Subscriptions are incremental.
+            Driver should keep track of requested resources.
+            Parameters:
+                subscriptions : List[str]
+                    List of tuples, each containing a resource_key pointing the resource to be subscribed, a
+                    sampling_duration, and a sampling_interval (both in seconds with float representation) defining,
+                    respectively, for how long monitoring should last, and the desired monitoring interval for the
+                    resource specified.
+            Returns:
+                results : List[Union[bool, Exception]]
+                    List of results for resource key unsubscriptions requested. Return values must be in the same order
+                    than resource keys requested. If a resource is properly unsubscribed, True must be retrieved;
+                    otherwise, the Exception that is raised during the processing must be retrieved.
+        """
+        raise NotImplementedError()
+
+    def GetState(self, blocking=False) -> Iterator[Tuple[float, str, Any]]:
+        """ Retrieve last collected values for subscribed resources. Operates as a generator, so this method should be
+            called once and will block until values are available. When values are available, it should yield each of
+            them and block again until new values are available. When the driver is destroyed, GetState() can return
+            instead of yield to terminate the loop.
+            Examples:
+                # keep looping waiting for extra samples (generator loop)
+                for timestamp,resource_key,resource_value in my_driver.GetState(blocking=True):
+                    process(timestamp, resource_key, resource_value)
+
+                # just retrieve accumulated samples
+                samples = my_driver.GetState(blocking=False)
+                # or (as classical loop)
+                for timestamp,resource_key,resource_value in my_driver.GetState(blocking=False):
+                    process(timestamp, resource_key, resource_value)
+            Parameters:
+                blocking : bool
+                    Select the driver behaviour. In both cases, the driver will first retrieve the samples accumulated
+                    and available in the internal queue. Then, if blocking, the driver does not terminate the loop and
+                    waits for additional samples to come, thus behaving as a generator. If non-blocking, the driver
+                    terminates the loop and returns. Non-blocking behaviour can be used for periodically polling the
+                    driver, while blocking can be used when a separate thread is in charge of collecting the samples
+                    produced by the driver.
+            Returns:
+                results : Iterator[Tuple[float, str, Any]]
+                    Sequences of state sample. Each State sample contains a float Unix-like timestamps of the samples in
+                    seconds with up to microsecond resolution, the resource_key of the sample, and its resource_value.
+                    Only resources with an active subscription must be retrieved. Interval and duration of the sampling
+                    process are specified when creating the subscription using method SubscribeState(). Order of values
+                    yielded is arbitrary.
+        """
+        raise NotImplementedError()
diff --git a/src/device/service/driver_api/__init__.py b/src/device/service/driver_api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/device/service/drivers/emulated/AnyTreeTools.py b/src/device/service/drivers/emulated/AnyTreeTools.py
new file mode 100644
index 0000000000000000000000000000000000000000..e7817b78921e3b91fb84ca660f022c869ac88220
--- /dev/null
+++ b/src/device/service/drivers/emulated/AnyTreeTools.py
@@ -0,0 +1,62 @@
+import anytree
+from typing import Any, List, Optional
+
+from anytree.render import Row
+
+class TreeNode(anytree.node.Node):
+    def __init__(self, name, parent=None, children=None, **kwargs) -> None:
+        super().__init__(name, parent=parent, children=children, **kwargs)
+        self.value : Optional[Any] = None
+
+    def get_full_path(self):
+        return self.separator.join([''] + [str(node.name) for node in self.path])
+
+class RawStyle(anytree.render.AbstractStyle):
+    def __init__(self):
+        """
+        Raw style.
+
+        >>> from anytree import Node, RenderTree
+        >>> root = Node("root")
+        >>> s0 = Node("sub0", parent=root)
+        >>> s0b = Node("sub0B", parent=s0)
+        >>> s0a = Node("sub0A", parent=s0)
+        >>> s1 = Node("sub1", parent=root)
+        >>> print(RenderTree(root, style=RawStyle()))
+        Node('/root')
+        Node('/root/sub0')
+        Node('/root/sub0/sub0B')
+        Node('/root/sub0/sub0A')
+        Node('/root/sub1')
+        """
+        super(RawStyle, self).__init__(u'', u'', u'')
+
+def get_subnode(resolver : anytree.Resolver, root : TreeNode, path : List[str], default : Optional[Any] = None):
+    node = root
+    for path_item in path:
+        try:
+            node = resolver.get(node, path_item)
+        except anytree.ChildResolverError:
+            return default
+    return node
+
+def set_subnode_value(resolver : anytree.Resolver, root : TreeNode, path : List[str], value : Any):
+    node = root
+    for path_item in path:
+        try:
+            node = resolver.get(node, path_item)
+        except anytree.ChildResolverError:
+            node = TreeNode(path_item, parent=node)
+    node.value = value
+
+def dump_subtree(root : TreeNode):
+    if not isinstance(root, TreeNode): raise Exception('root must be a TreeNode')
+    results = []
+    for row in anytree.RenderTree(root, style=RawStyle()):
+        node : TreeNode = row.node
+        path = node.get_full_path()[2:] # get full path except the heading root placeholder "/."
+        if len(path) == 0: continue
+        value = node.value
+        if value is None: continue
+        results.append((path, value))
+    return results
diff --git a/src/device/service/drivers/emulated/EmulatedDriver.py b/src/device/service/drivers/emulated/EmulatedDriver.py
new file mode 100644
index 0000000000000000000000000000000000000000..7dc5757f7afb928bd56e64b5a87d4c414171cd6b
--- /dev/null
+++ b/src/device/service/drivers/emulated/EmulatedDriver.py
@@ -0,0 +1,201 @@
+import anytree, logging, pytz, queue, random, threading
+from datetime import datetime, timedelta
+from typing import Any, Iterator, List, Tuple, Union
+from apscheduler.executors.pool import ThreadPoolExecutor
+from apscheduler.job import Job
+from apscheduler.jobstores.memory import MemoryJobStore
+from apscheduler.schedulers.background import BackgroundScheduler
+from common.Checkers import chk_float, chk_length, chk_string, chk_type
+from device.service.driver_api._Driver import _Driver
+from .AnyTreeTools import TreeNode, dump_subtree, get_subnode, set_subnode_value
+
+LOGGER = logging.getLogger(__name__)
+
+def sample(resource_key : str, out_samples : queue.Queue):
+    out_samples.put_nowait((datetime.timestamp(datetime.utcnow()), resource_key, random.random()))
+
+class EmulatedDriver(_Driver):
+    def __init__(self, address : str, port : int, **kwargs) -> None:
+        self.__lock = threading.Lock()
+        self.__root = TreeNode('.')
+        self.__terminate = threading.Event()
+        self.__scheduler = BackgroundScheduler(daemon=True) # scheduler used to emulate sampling events
+        self.__scheduler.configure(
+            jobstores = {'default': MemoryJobStore()},
+            executors = {'default': ThreadPoolExecutor(max_workers=1)},
+            job_defaults = {'coalesce': False, 'max_instances': 3},
+            timezone=pytz.utc)
+        self.__out_samples = queue.Queue()
+
+    def Connect(self) -> bool:
+        # Connect triggers activation of sampling events that will be scheduled based on subscriptions
+        self.__scheduler.start()
+        return True
+
+    def Disconnect(self) -> bool:
+        # Trigger termination of loops and processes
+        self.__terminate.set()
+        # Disconnect triggers deactivation of sampling events
+        self.__scheduler.shutdown()
+        return True
+
+    def GetConfig(self, resource_keys : List[str] = []) -> List[Union[Any, None, Exception]]:
+        chk_type('resources', resource_keys, list)
+        with self.__lock:
+            if len(resource_keys) == 0: return dump_subtree(self.__root)
+            results = []
+            resolver = anytree.Resolver(pathattr='name')
+            for i,resource_key in enumerate(resource_keys):
+                str_resource_name = 'resource_key[#{:d}]'.format(i)
+                try:
+                    chk_string(str_resource_name, resource_key, allow_empty=False)
+                    resource_path = resource_key.split('/')
+                except Exception as e:
+                    LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key)))
+                    results.append(e) # if validation fails, store the exception
+                    continue
+
+                resource_node = get_subnode(resolver, self.__root, resource_path, default=None)
+                # if not found, resource_node is None
+                results.append(None if resource_node is None else dump_subtree(resource_node))
+            return results
+
+    def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]:
+        chk_type('resources', resources, list)
+        if len(resources) == 0: return []
+        results = []
+        resolver = anytree.Resolver(pathattr='name')
+        with self.__lock:
+            for i,resource in enumerate(resources):
+                str_resource_name = 'resources[#{:d}]'.format(i)
+                try:
+                    chk_type(str_resource_name, resource, (list, tuple))
+                    chk_length(str_resource_name, resource, allowed_lengths=2)
+                    resource_key,resource_value = resource
+                    resource_path = resource_key.split('/')
+                except Exception as e:
+                    LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key)))
+                    results.append(e) # if validation fails, store the exception
+                    continue
+
+                set_subnode_value(resolver, self.__root, resource_path, resource_value)
+                results.append(True)
+        return results
+
+    def DeleteConfig(self, resource_keys : List[str]) -> List[Union[bool, Exception]]:
+        chk_type('resources', resource_keys, list)
+        if len(resource_keys) == 0: return []
+        results = []
+        resolver = anytree.Resolver(pathattr='name')
+        with self.__lock:
+            for i,resource_key in enumerate(resource_keys):
+                str_resource_name = 'resource_key[#{:d}]'.format(i)
+                try:
+                    chk_string(str_resource_name, resource_key, allow_empty=False)
+                    resource_path = resource_key.split('/')
+                except Exception as e:
+                    LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key)))
+                    results.append(e) # if validation fails, store the exception
+                    continue
+
+                resource_node = get_subnode(resolver, self.__root, resource_path, default=None)
+                # if not found, resource_node is None
+                if resource_node is None:
+                    results.append(False)
+                    continue
+
+                parent = resource_node.parent
+                children = list(parent.children)
+                children.remove(resource_node)
+                parent.children = tuple(children)
+                results.append(True)
+        return results
+
+    def SubscribeState(self, resources : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]:
+        chk_type('resources', resources, list)
+        if len(resources) == 0: return []
+        results = []
+        resolver = anytree.Resolver(pathattr='name')
+        with self.__lock:
+            for i,resource in enumerate(resources):
+                str_resource_name = 'resources[#{:d}]'.format(i)
+                try:
+                    chk_type(str_resource_name, resource, (list, tuple))
+                    chk_length(str_resource_name, resource, allowed_lengths=3)
+                    resource_key,sampling_duration,sampling_interval = resource
+                    chk_string(str_resource_name + '.resource_key', resource_key, allow_empty=False)
+                    resource_path = resource_key.split('/')
+                    chk_float(str_resource_name + '.sampling_duration', sampling_duration, min_value=0)
+                    chk_float(str_resource_name + '.sampling_interval', sampling_interval, min_value=0)
+                except Exception as e:
+                    LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key)))
+                    results.append(e) # if validation fails, store the exception
+                    continue
+
+                start_date,end_date = None,None
+                if sampling_duration <= 1.e-12:
+                    start_date = datetime.utcnow()
+                    end_date = start_date + timedelta(seconds=sampling_duration)
+
+                job_id = 'k={:s}/d={:f}/i={:f}'.format(resource_key, sampling_duration, sampling_interval)
+                job = self.__scheduler.add_job(
+                    sample, args=(resource_key, self.__out_samples), kwargs={},
+                    id=job_id, trigger='interval', seconds=sampling_interval,
+                    start_date=start_date, end_date=end_date, timezone=pytz.utc)
+
+                subscription_path = resource_path + ['{:.3f}:{:.3f}'.format(sampling_duration, sampling_interval)]
+                set_subnode_value(resolver, self.__root, subscription_path, job)
+                results.append(True)
+        return results
+
+    def UnsubscribeState(self, resources : List[Tuple[str, float, float]]) -> List[Union[bool, Exception]]:
+        chk_type('resources', resources, list)
+        if len(resources) == 0: return []
+        results = []
+        resolver = anytree.Resolver(pathattr='name')
+        with self.__lock:
+            for i,resource in enumerate(resources):
+                str_resource_name = 'resources[#{:d}]'.format(i)
+                try:
+                    chk_type(str_resource_name, resource, (list, tuple))
+                    chk_length(str_resource_name, resource, allowed_lengths=3)
+                    resource_key,sampling_duration,sampling_interval = resource
+                    chk_string(str_resource_name + '.resource_key', resource_key, allow_empty=False)
+                    resource_path = resource_key.split('/')
+                    chk_float(str_resource_name + '.sampling_duration', sampling_duration, min_value=0)
+                    chk_float(str_resource_name + '.sampling_interval', sampling_interval, min_value=0)
+                except Exception as e:
+                    LOGGER.exception('Exception validating {}: {}'.format(str_resource_name, str(resource_key)))
+                    results.append(e) # if validation fails, store the exception
+                    continue
+
+                subscription_path = resource_path + ['{:.3f}:{:.3f}'.format(sampling_duration, sampling_interval)]
+                subscription_node = get_subnode(resolver, self.__root, subscription_path)
+                
+                # if not found, resource_node is None
+                if subscription_node is None:
+                    results.append(False)
+                    continue
+
+                job : Job = getattr(subscription_node, 'value', None)
+                if job is None or not isinstance(job, Job):
+                    raise Exception('Malformed subscription node or wrong resource key: {}'.format(str(resource)))
+                job.remove()
+
+                parent = subscription_node.parent
+                children = list(parent.children)
+                children.remove(subscription_node)
+                parent.children = tuple(children)
+
+                results.append(True)
+        return results
+
+    def GetState(self, blocking=False) -> Iterator[Tuple[str, Any]]:
+        while not self.__terminate.is_set():
+            try:
+                sample = self.__out_samples.get(block=blocking, timeout=0.1)
+            except queue.Empty:
+                if blocking: continue
+                return
+            if sample is None: continue
+            yield sample
diff --git a/src/device/service/drivers/emulated/QueryFields.py b/src/device/service/drivers/emulated/QueryFields.py
new file mode 100644
index 0000000000000000000000000000000000000000..6db43e5b5d4ffe1bbcc652d305981757bd960c3e
--- /dev/null
+++ b/src/device/service/drivers/emulated/QueryFields.py
@@ -0,0 +1,8 @@
+from enum import Enum
+
+VENDOR_CTTC = 'cttc'
+
+DEVICE_MODEL_EMULATED_OPTICAL_ROADM       = 'cttc_emu_opt_rdm'
+DEVICE_MODEL_EMULATED_OPTICAL_TRANDPONDER = 'cttc_emu_opt_tp'
+DEVICE_MODEL_EMULATED_PACKET_ROUTER       = 'cttc_emu_pkt_rtr'
+DEVICE_MODEL_EMULATED_PACKET_SWITCH       = 'cttc_emu_pkt_swt'
diff --git a/src/device/service/drivers/emulated/__init__.py b/src/device/service/drivers/emulated/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/device/service/drivers/openconfig/__init__.py b/src/device/service/drivers/openconfig/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/device/tests/test_unitary_driverapi.py b/src/device/tests/test_unitary_driverapi.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac7231cf76d4bcba0ea37da0cab781e21bc1c560
--- /dev/null
+++ b/src/device/tests/test_unitary_driverapi.py
@@ -0,0 +1,128 @@
+import copy, logging, math, pytest, time
+from device.service.drivers.emulated.EmulatedDriver import EmulatedDriver
+
+LOGGER = logging.getLogger(__name__)
+LOGGER.setLevel(logging.DEBUG)
+
+PATH_IF = '/interfaces/interface[name="{}"]'
+PATH_SUBIF = PATH_IF + '/subinterfaces/subinterface[index="{}"]'
+PATH_ADDRIPV4 = PATH_SUBIF + '/ipv4/address[ip="{}"]'
+
+DEVICE_CONFIG_IF1 = []
+DEVICE_CONFIG_IF1.append((PATH_IF      .format('IF1'               ) + '/config/name',           "IF1"     ))
+DEVICE_CONFIG_IF1.append((PATH_IF      .format('IF1'               ) + '/config/enabled',        True      ))
+DEVICE_CONFIG_IF1.append((PATH_SUBIF   .format('IF1', 0            ) + '/config/index',          0         ))
+DEVICE_CONFIG_IF1.append((PATH_ADDRIPV4.format('IF1', 0, '10.1.0.1') + '/config/ip',             "10.1.0.1"))
+DEVICE_CONFIG_IF1.append((PATH_ADDRIPV4.format('IF1', 0, '10.1.0.1') + '/config/prefix_length',  24        ))
+
+DEVICE_CONFIG_IF2 = []
+DEVICE_CONFIG_IF2.append((PATH_IF      .format('IF2'               ) + '/config/name',           "IF2"     ))
+DEVICE_CONFIG_IF2.append((PATH_IF      .format('IF2'               ) + '/config/enabled',        True      ))
+DEVICE_CONFIG_IF2.append((PATH_SUBIF   .format('IF2', 0            ) + '/config/index',          0         ))
+DEVICE_CONFIG_IF2.append((PATH_ADDRIPV4.format('IF2', 0, '10.2.0.1') + '/config/ip',             "10.2.0.1"))
+DEVICE_CONFIG_IF2.append((PATH_ADDRIPV4.format('IF2', 0, '10.2.0.1') + '/config/prefix_length',  24        ))
+
+PATH_IF_TX_PKTS = PATH_IF + 'state/tx_packets_per_second'
+PATH_IF_RX_PKTS = PATH_IF + 'state/rx_packets_per_second'
+
+DEVICE_STATE_IF1_TX_PKTS = PATH_IF_TX_PKTS.format('IF1')
+DEVICE_STATE_IF1_RX_PKTS = PATH_IF_RX_PKTS.format('IF1')
+DEVICE_STATE_IF2_TX_PKTS = PATH_IF_TX_PKTS.format('IF2')
+DEVICE_STATE_IF2_RX_PKTS = PATH_IF_RX_PKTS.format('IF2')
+
+@pytest.fixture(scope='session')
+def device_driverapi_emulated():
+    _driver = EmulatedDriver('127.0.0.1', 0)
+    _driver.Connect()
+    yield _driver
+    _driver.Disconnect()
+
+def test_device_driverapi_emulated_setconfig(device_driverapi_emulated : EmulatedDriver):
+    # should work
+    results = device_driverapi_emulated.SetConfig(DEVICE_CONFIG_IF1)
+    LOGGER.info('results:\n{}'.format('\n'.join(map(str, results))))
+    assert len(results) == len(DEVICE_CONFIG_IF1)
+    for result in results: assert isinstance(result, bool) and result
+
+    results = device_driverapi_emulated.SetConfig(DEVICE_CONFIG_IF2)
+    LOGGER.info('results:\n{}'.format('\n'.join(map(str, results))))
+    assert len(results) == len(DEVICE_CONFIG_IF2)
+    for result in results: assert isinstance(result, bool) and result
+
+def test_device_driverapi_emulated_getconfig(device_driverapi_emulated : EmulatedDriver):
+    stored_config = device_driverapi_emulated.GetConfig()
+    LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config))))
+    assert len(stored_config) == len(DEVICE_CONFIG_IF1) + len(DEVICE_CONFIG_IF2)
+    for config_row in stored_config: assert (config_row in DEVICE_CONFIG_IF1) or (config_row in DEVICE_CONFIG_IF2)
+    for config_row in DEVICE_CONFIG_IF1: assert config_row in stored_config
+    for config_row in DEVICE_CONFIG_IF2: assert config_row in stored_config
+
+    # should work
+    stored_config = device_driverapi_emulated.GetConfig([PATH_IF.format('IF2')])
+    LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config))))
+    assert len(stored_config) == 1
+    stored_config = stored_config[0]
+    LOGGER.info('stored_config[0]:\n{}'.format('\n'.join(map(str, stored_config))))
+    assert len(stored_config) == len(DEVICE_CONFIG_IF2)
+    for config_row in stored_config: assert config_row in DEVICE_CONFIG_IF2
+    for config_row in DEVICE_CONFIG_IF2: assert config_row in stored_config
+
+def test_device_driverapi_emulated_deleteconfig(device_driverapi_emulated : EmulatedDriver):
+    # should work
+    results = device_driverapi_emulated.DeleteConfig([PATH_ADDRIPV4.format('IF2', 0, '10.2.0.1')])
+    LOGGER.info('results:\n{}'.format('\n'.join(map(str, results))))
+    assert (len(results) == 1) and isinstance(results[0], bool) and results[0]
+
+    stored_config = device_driverapi_emulated.GetConfig()
+    LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config))))
+
+    device_config_if2 = list(filter(lambda row: '10.2.0.1' not in row[0], copy.deepcopy(DEVICE_CONFIG_IF2)))
+    assert len(stored_config) == len(DEVICE_CONFIG_IF1) + len(device_config_if2)
+    for config_row in stored_config: assert (config_row in DEVICE_CONFIG_IF1) or (config_row in device_config_if2)
+    for config_row in DEVICE_CONFIG_IF1: assert config_row in stored_config
+    for config_row in device_config_if2: assert config_row in stored_config
+
+def test_device_driverapi_emulated_subscriptions(device_driverapi_emulated : EmulatedDriver):
+    # should work
+    duration = 10.0
+    interval = 1.5
+    results = device_driverapi_emulated.SubscribeState([
+        (DEVICE_STATE_IF1_TX_PKTS, duration, interval),
+        (DEVICE_STATE_IF1_RX_PKTS, duration, interval),
+        (DEVICE_STATE_IF2_TX_PKTS, duration, interval),
+        (DEVICE_STATE_IF2_RX_PKTS, duration, interval),
+    ])
+    LOGGER.info('results:\n{}'.format('\n'.join(map(str, results))))
+    assert len(results) == 4
+    for result in results: assert isinstance(result, bool) and result
+
+    stored_config = device_driverapi_emulated.GetConfig()
+    LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config))))
+
+    time.sleep(duration + 1.0) # let time to generate samples, plus 1 second extra time
+
+    samples = []
+    for sample in device_driverapi_emulated.GetState(blocking=False):
+        LOGGER.info('sample: {}'.format(str(sample)))
+        timestamp,resource_key,resource_value = sample
+        samples.append((timestamp, resource_key, resource_value))
+    LOGGER.info('samples:\n{}'.format('\n'.join(map(str, samples))))
+    assert len(samples) == 4 * (math.floor(duration/interval) + 1)
+
+    results = device_driverapi_emulated.UnsubscribeState([
+        (DEVICE_STATE_IF1_TX_PKTS, 10.0, 1.5),
+        (DEVICE_STATE_IF1_RX_PKTS, 10.0, 1.5),
+        (DEVICE_STATE_IF2_TX_PKTS, 10.0, 1.5),
+        (DEVICE_STATE_IF2_RX_PKTS, 10.0, 1.5),
+    ])
+    LOGGER.info('results:\n{}'.format('\n'.join(map(str, results))))
+    assert len(results) == 4
+    for result in results: assert isinstance(result, bool) and result
+
+    stored_config = device_driverapi_emulated.GetConfig()
+    LOGGER.info('stored_config:\n{}'.format('\n'.join(map(str, stored_config))))
+    device_config_if2 = list(filter(lambda row: '10.2.0.1' not in row[0], copy.deepcopy(DEVICE_CONFIG_IF2)))
+    assert len(stored_config) == len(DEVICE_CONFIG_IF1) + len(device_config_if2)
+    for config_row in stored_config: assert (config_row in DEVICE_CONFIG_IF1) or (config_row in device_config_if2)
+    for config_row in DEVICE_CONFIG_IF1: assert config_row in stored_config
+    for config_row in device_config_if2: assert config_row in stored_config
diff --git a/src/device/tests/test_unitary.py b/src/device/tests/test_unitary_service.py
similarity index 79%
rename from src/device/tests/test_unitary.py
rename to src/device/tests/test_unitary_service.py
index 95eb0a1af27433ddd85d100160cd122aebc60b8a..8d9591dd6492395a44adc25e4a54eaebc8ff9121 100644
--- a/src/device/tests/test_unitary.py
+++ b/src/device/tests/test_unitary_service.py
@@ -65,7 +65,9 @@ def test_add_device_wrong_attributes(device_client : DeviceClient):
         copy_device['device_id']['device_id']['uuid'] = ''
         device_client.AddDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'device.device_id.device_id.uuid() string is empty.'
+    msg = 'device.device_id.device_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
     # should fail with device type is empty
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
@@ -73,7 +75,9 @@ def test_add_device_wrong_attributes(device_client : DeviceClient):
         copy_device['device_type'] = ''
         device_client.AddDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'device.device_type() string is empty.'
+    msg = 'device.device_type() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
     # should fail with wrong device operational status
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
@@ -81,10 +85,8 @@ def test_add_device_wrong_attributes(device_client : DeviceClient):
         copy_device['devOperationalStatus'] = OperationalStatus.KEEP_STATE.value
         device_client.AddDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Method(AddDevice) does not accept OperationalStatus(KEEP_STATE).',
-        'Permitted values for Method(AddDevice) are OperationalStatus([\'DISABLED\', \'ENABLED\']).',
-    ])
+    msg = 'Method(AddDevice) does not accept OperationalStatus(KEEP_STATE). '\
+          'Permitted values for Method(AddDevice) are OperationalStatus([\'DISABLED\', \'ENABLED\']).'
     assert e.value.details() == msg
 
 def test_add_device_wrong_endpoint(device_client : DeviceClient):
@@ -95,11 +97,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient):
         request = Device(**copy_device)
         device_client.AddDevice(request)
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Context(wrong-context)',
-        'in Endpoint(#0) of Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Contexts({\'admin\'}).',
-        'Optionally, leave field empty to use predefined Context(admin).',
-    ])
+    msg = 'Context(wrong-context) in Endpoint(#0) of '\
+          'Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Contexts({\'admin\'}). '\
+          'Optionally, leave field empty to use predefined Context(admin).'
     assert e.value.details() == msg
 
     # should fail with unsupported endpoint topology
@@ -108,11 +108,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient):
         copy_device['endpointList'][0]['port_id']['topoId']['topoId']['uuid'] = 'wrong-topo'
         device_client.AddDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Context(admin)/Topology(wrong-topo)',
-        'in Endpoint(#0) of Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Topologies({\'admin\'}).',
-        'Optionally, leave field empty to use predefined Topology(admin).',
-    ])
+    msg = 'Context(admin)/Topology(wrong-topo) in Endpoint(#0) of '\
+          'Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Topologies({\'admin\'}). '\
+          'Optionally, leave field empty to use predefined Topology(admin).'
     assert e.value.details() == msg
 
     # should fail with wrong endpoint device
@@ -121,11 +119,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient):
         copy_device['endpointList'][0]['port_id']['dev_id']['device_id']['uuid'] = 'wrong-device'
         device_client.AddDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Context(admin)/Topology(admin)/Device(wrong-device)',
-        'in Endpoint(#0) of Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Devices({\'DEV1\'}).',
-        'Optionally, leave field empty to use predefined Device(DEV1).',
-    ])
+    msg = 'Context(admin)/Topology(admin)/Device(wrong-device) in Endpoint(#0) of '\
+          'Context(admin)/Topology(admin)/Device(DEV1) mismatches acceptable Devices({\'DEV1\'}). '\
+          'Optionally, leave field empty to use predefined Device(DEV1).'
     assert e.value.details() == msg
 
     # should fail with endpoint port uuid is empty
@@ -134,7 +130,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient):
         copy_device['endpointList'][0]['port_id']['port_id']['uuid'] = ''
         device_client.AddDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'endpoint_id[#0].port_id.uuid() string is empty.'
+    msg = 'endpoint_id[#0].port_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
     # should fail with endpoint port type is empty
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
@@ -142,7 +140,9 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient):
         copy_device['endpointList'][0]['port_type'] = ''
         device_client.AddDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'endpoint[#0].port_type() string is empty.'
+    msg = 'endpoint[#0].port_type() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
     # should fail with duplicate port in device
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
@@ -150,10 +150,8 @@ def test_add_device_wrong_endpoint(device_client : DeviceClient):
         copy_device['endpointList'][1]['port_id']['port_id']['uuid'] = 'EP2'
         device_client.AddDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Duplicated Context(admin)/Topology(admin)/Device(DEV1)/Port(EP2)',
-        'in Endpoint(#1) of Context(admin)/Topology(admin)/Device(DEV1).',
-    ])
+    msg = 'Duplicated Context(admin)/Topology(admin)/Device(DEV1)/Port(EP2) in Endpoint(#1) of '\
+          'Context(admin)/Topology(admin)/Device(DEV1).'
     assert e.value.details() == msg
 
 def test_add_device(device_client : DeviceClient):
@@ -168,7 +166,8 @@ def test_add_device_duplicate(device_client : DeviceClient):
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
         device_client.AddDevice(Device(**DEVICE))
     assert e.value.code() == grpc.StatusCode.ALREADY_EXISTS
-    assert e.value.details() == 'Context(admin)/Topology(admin)/Device(DEV1) already exists in the database.'
+    msg = 'Context(admin)/Topology(admin)/Device(DEV1) already exists in the database.'
+    assert e.value.details() == msg
 
 def test_delete_device_empty_uuid(device_client : DeviceClient):
     # should fail with device uuid is empty
@@ -177,7 +176,9 @@ def test_delete_device_empty_uuid(device_client : DeviceClient):
         copy_device_id['device_id']['uuid'] = ''
         device_client.DeleteDevice(DeviceId(**copy_device_id))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'device_id.device_id.uuid() string is empty.'
+    msg = 'device_id.device_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
 def test_delete_device_not_found(device_client : DeviceClient):
     # should fail with device not found
@@ -186,7 +187,8 @@ def test_delete_device_not_found(device_client : DeviceClient):
         copy_device_id['device_id']['uuid'] = 'wrong-device-id'
         device_client.DeleteDevice(DeviceId(**copy_device_id))
     assert e.value.code() == grpc.StatusCode.NOT_FOUND
-    assert e.value.details() == 'Context(admin)/Topology(admin)/Device(wrong-device-id) does not exist in the database.'
+    msg = 'Context(admin)/Topology(admin)/Device(wrong-device-id) does not exist in the database.'
+    assert e.value.details() == msg
 
 def test_delete_device(device_client : DeviceClient):
     # should work
@@ -202,7 +204,9 @@ def test_configure_device_empty_device_uuid(device_client : DeviceClient):
         copy_device['device_id']['device_id']['uuid'] = ''
         device_client.ConfigureDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'device.device_id.device_id.uuid() string is empty.'
+    msg = 'device.device_id.device_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
+    assert e.value.details() == msg
 
 def test_configure_device_not_found(device_client : DeviceClient):
     # should fail with device not found
@@ -211,7 +215,8 @@ def test_configure_device_not_found(device_client : DeviceClient):
         copy_device['device_id']['device_id']['uuid'] = 'wrong-device-id'
         device_client.ConfigureDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.NOT_FOUND
-    assert e.value.details() == 'Context(admin)/Topology(admin)/Device(wrong-device-id) does not exist in the database.'
+    msg = 'Context(admin)/Topology(admin)/Device(wrong-device-id) does not exist in the database.'
+    assert e.value.details() == msg
 
 def test_add_device_default_endpoint_context_topology_device(device_client : DeviceClient):
     # should work
@@ -231,7 +236,8 @@ def test_configure_device_wrong_attributes(device_client : DeviceClient):
         copy_device['device_type'] = 'wrong-type'
         device_client.ConfigureDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    assert e.value.details() == 'Device(DEV1) has Type(ROADM) in the database. Cannot be changed to Type(wrong-type).'
+    msg = 'Device(DEV1) has Type(ROADM) in the database. Cannot be changed to Type(wrong-type).'
+    assert e.value.details() == msg
 
     # should fail with endpoints cannot be modified
     with pytest.raises(grpc._channel._InactiveRpcError) as e:
@@ -248,10 +254,8 @@ def test_configure_device_wrong_attributes(device_client : DeviceClient):
         copy_device['endpointList'].clear()
         device_client.ConfigureDevice(Device(**copy_device))
     assert e.value.code() == grpc.StatusCode.ABORTED
-    msg = ' '.join([
-        'Any change has been requested for Device(DEV1).',
-        'Either specify a new configuration or a new device operational status.',
-    ])
+    msg = 'Any change has been requested for Device(DEV1). '\
+          'Either specify a new configuration or a new device operational status.'
     assert e.value.details() == msg
 
 def test_configure_device(device_client : DeviceClient):
diff --git a/src/service/tests/test_unitary.py b/src/service/tests/test_unitary.py
index 48d56af038b226caa2ffe6a73b71509f4c011ad1..fb7d1465d3308261e0f2bdc5bc534f67d89fae1e 100644
--- a/src/service/tests/test_unitary.py
+++ b/src/service/tests/test_unitary.py
@@ -71,7 +71,8 @@ def test_create_service_wrong_service_attributes(service_client : ServiceClient)
         copy_service['cs_id']['contextId']['contextUuid']['uuid'] = ''
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = 'service.cs_id.contextId.contextUuid.uuid() string is empty.'
+    msg = 'service.cs_id.contextId.contextUuid.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
     assert e.value.details() == msg
 
     # should fail with service context does not exist
@@ -89,7 +90,8 @@ def test_create_service_wrong_service_attributes(service_client : ServiceClient)
         copy_service['cs_id']['cs_id']['uuid'] = ''
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = 'service.cs_id.cs_id.uuid() string is empty.'
+    msg = 'service.cs_id.cs_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
     assert e.value.details() == msg
 
     # should fail with wrong service type
@@ -98,11 +100,9 @@ def test_create_service_wrong_service_attributes(service_client : ServiceClient)
         copy_service['serviceType'] = ServiceType.UNKNOWN
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Method(CreateService) does not accept ServiceType(UNKNOWN).',
-        'Permitted values for Method(CreateService) are',
-        'ServiceType([\'L2NM\', \'L3NM\', \'TAPI_CONNECTIVITY_SERVICE\']).',
-    ])
+    msg = 'Method(CreateService) does not accept ServiceType(UNKNOWN). '\
+          'Permitted values for Method(CreateService) are '\
+          'ServiceType([\'L2NM\', \'L3NM\', \'TAPI_CONNECTIVITY_SERVICE\']).'
     assert e.value.details() == msg
 
     # should fail with wrong service state
@@ -111,11 +111,9 @@ def test_create_service_wrong_service_attributes(service_client : ServiceClient)
         copy_service['serviceState']['serviceState'] = ServiceStateEnum.PENDING_REMOVAL
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Method(CreateService) does not accept ServiceState(PENDING_REMOVAL).',
-        'Permitted values for Method(CreateService) are',
-        'ServiceState([\'PLANNED\']).',
-    ])
+    msg = 'Method(CreateService) does not accept ServiceState(PENDING_REMOVAL). '\
+          'Permitted values for Method(CreateService) are '\
+          'ServiceState([\'PLANNED\']).'
     assert e.value.details() == msg
 
 def test_create_service_wrong_constraint(service_client : ServiceClient):
@@ -125,7 +123,8 @@ def test_create_service_wrong_constraint(service_client : ServiceClient):
         copy_service['constraint'][0]['constraint_type'] = ''
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = 'constraint[#0].constraint_type() string is empty.'
+    msg = 'constraint[#0].constraint_type() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
     assert e.value.details() == msg
 
     # should fail with wrong constraint value
@@ -134,7 +133,8 @@ def test_create_service_wrong_constraint(service_client : ServiceClient):
         copy_service['constraint'][0]['constraint_value'] = ''
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = 'constraint[#0].constraint_value() string is empty.'
+    msg = 'constraint[#0].constraint_value() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
     assert e.value.details() == msg
 
     # should fail with dupplicated constraint type
@@ -154,11 +154,9 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database
         print(copy_service)
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Context(wrong-context) in Endpoint(#0) of Context(admin)/Service(DEV1)',
-        'mismatches acceptable Contexts({\'admin\'}).',
-        'Optionally, leave field empty to use predefined Context(admin).'
-    ])
+    msg = 'Context(wrong-context) in Endpoint(#0) of '\
+          'Context(admin)/Service(DEV1) mismatches acceptable Contexts({\'admin\'}). '\
+          'Optionally, leave field empty to use predefined Context(admin).'
     assert e.value.details() == msg
 
     # should fail with wrong endpoint topology
@@ -167,11 +165,9 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database
         copy_service['endpointList'][0]['topoId']['topoId']['uuid'] = 'wrong-topo'
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = ' '.join([
-        'Context(admin)/Topology(wrong-topo) in Endpoint(#0) of Context(admin)/Service(DEV1)',
-        'mismatches acceptable Topologies({\'admin\'}).',
-        'Optionally, leave field empty to use predefined Topology(admin).',
-    ])
+    msg = 'Context(admin)/Topology(wrong-topo) in Endpoint(#0) of '\
+          'Context(admin)/Service(DEV1) mismatches acceptable Topologies({\'admin\'}). '\
+          'Optionally, leave field empty to use predefined Topology(admin).'
     assert e.value.details() == msg
 
     # should fail with endpoint device is empty
@@ -180,7 +176,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database
         copy_service['endpointList'][0]['dev_id']['device_id']['uuid'] = ''
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = 'endpoint_id[#0].dev_id.device_id.uuid() string is empty.'
+    msg = 'endpoint_id[#0].dev_id.device_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
     assert e.value.details() == msg
 
     # should fail with endpoint device not found
@@ -189,10 +186,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database
         copy_service['endpointList'][0]['dev_id']['device_id']['uuid'] = 'wrong-device'
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.NOT_FOUND
-    msg = ' '.join([
-        'Context(admin)/Topology(admin)/Device(wrong-device) in Endpoint(#0)',
-        'of Context(admin)/Service(DEV1) does not exist in the database.',
-    ])
+    msg = 'Context(admin)/Topology(admin)/Device(wrong-device) in Endpoint(#0) of '\
+          'Context(admin)/Service(DEV1) does not exist in the database.'
     assert e.value.details() == msg
 
     # should fail with endpoint device duplicated
@@ -201,7 +196,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database
         copy_service['endpointList'][1] = copy_service['endpointList'][0]
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = 'Duplicated Context(admin)/Topology(admin)/Device(DEV1) in Endpoint(#1) of Context(admin)/Service(DEV1).'
+    msg = 'Duplicated Context(admin)/Topology(admin)/Device(DEV1) in Endpoint(#1) of '\
+          'Context(admin)/Service(DEV1).'
     assert e.value.details() == msg
 
     # should fail with endpoint port is empty
@@ -210,7 +206,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database
         copy_service['endpointList'][0]['port_id']['uuid'] = ''
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = 'endpoint_id[#0].port_id.uuid() string is empty.'
+    msg = 'endpoint_id[#0].port_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
     assert e.value.details() == msg
 
     # should fail with endpoint port not found
@@ -219,10 +216,8 @@ def test_create_service_wrong_endpoint(service_client : ServiceClient, database
         copy_service['endpointList'][0]['port_id']['uuid'] = 'wrong-port'
         service_client.CreateService(Service(**copy_service))
     assert e.value.code() == grpc.StatusCode.NOT_FOUND
-    msg = ' '.join([
-        'Context(admin)/Topology(admin)/Device(DEV1)/Port(wrong-port) in Endpoint(#0)',
-        'of Context(admin)/Service(DEV1) does not exist in the database.',
-    ])
+    msg = 'Context(admin)/Topology(admin)/Device(DEV1)/Port(wrong-port) in Endpoint(#0) of '\
+          'Context(admin)/Service(DEV1) does not exist in the database.'
     assert e.value.details() == msg
 
 def test_get_service_does_not_exist(service_client : ServiceClient):
@@ -305,7 +300,8 @@ def test_delete_service_wrong_service_id(service_client : ServiceClient):
         copy_service_id['contextId']['contextUuid']['uuid'] = ''
         service_client.DeleteService(ServiceId(**copy_service_id))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = 'service_id.contextId.contextUuid.uuid() string is empty.'
+    msg = 'service_id.contextId.contextUuid.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
     assert e.value.details() == msg
 
     # should fail with service context does not exist
@@ -323,7 +319,8 @@ def test_delete_service_wrong_service_id(service_client : ServiceClient):
         copy_service_id['cs_id']['uuid'] = ''
         service_client.DeleteService(ServiceId(**copy_service_id))
     assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
-    msg = 'service_id.cs_id.uuid() string is empty.'
+    msg = 'service_id.cs_id.uuid() is out of range: '\
+          'allow_empty(False) min_length(None) max_length(None) allowed_lengths(None).'
     assert e.value.details() == msg
 
     # should fail with service id is empty
diff --git a/src/tester_integration/.gitlab-ci.yml b/src/tester_integration/.gitlab-ci.yml
index cf868b1a40c583481b292dfe2445b3e0fcfe4501..02eccbc9bf9fc1103b373b4d062333c42f52b87e 100644
--- a/src/tester_integration/.gitlab-ci.yml
+++ b/src/tester_integration/.gitlab-ci.yml
@@ -54,6 +54,7 @@ integ_test execute:
     - unit_test device
     - unit_test service
     - unit_test integ_test
+    - dependencies all
   before_script:
     - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
     - if docker network list | grep teraflowbridge; then echo "teraflowbridge is already created"; else docker network create -d bridge teraflowbridge; fi