diff --git a/README.md b/README.md
index d4f408b4ea3fc7ebfad35dcef303669acc49b063..99c756984e8dfe60fd5ae159dd6a04898163e469 100644
--- a/README.md
+++ b/README.md
@@ -80,13 +80,20 @@ Most notably, you will have web interfaces available at:
 
 # Development
 
-For development you will need several dependencies including:
+For development, you will need several dependencies including:
 
 ```
 git g++ gcc make docker docker-compose shellcheck graphviz python3-dev \
 python3-pip python3-tox libboost-python-dev libtango-cpp pkg-config
 ```
 
+Please source `setup.sh` file prior to developing to install any git hooks
+and setup environment variables.
+
+```sh
+source setup.sh
+```
+
 Of these docker-compose must be at least 2.0 and Python 3.10 or higher.
 Alternatively, tox can be installed through pip using `pip install tox`.
 
@@ -110,8 +117,10 @@ Next change the version in the following places:
 
 # Release Notes
 
+* 0.12.1 Add `AbstractHierarchy` and `AbstractHierarchyDevice` classes and
+         functionality
 * 0.12.0 Add `Calibration_SDP_Subband_Weights_<XXX>MHz_R` attributes to implement HDF5 calibration tables
-* 0.11.2 Fix sleep duration in archiver test 
+* 0.11.2 Fix sleep duration in archiver test
 * 0.11.1 Fix event unsubscription in TemperatureManager
 * 0.11.0 Added StationManager device
 * 0.10.0 Add `AntennaToSdpMapper` and fpga_sdp_info_* mapped attributes in `Antennafield` device
diff --git a/setup.sh b/setup.sh
index 23a98f323d7d01a18b7f341dc763eb1490095dab..ecab83f49583b812de428516c5b23a03ef9be651 100644
--- a/setup.sh
+++ b/setup.sh
@@ -2,53 +2,28 @@
 # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-# Set up the LOFAR2.0 environment.
-# For the time being it is assumend that the LOFAR2.0 environment has to
-# co-exist with a LOFAR1 environment.
+# Set up station control development
 
-# Set these for the host where you run SKA's Tango Docker images.
-# And export those directories for LOFAR in Tango Docker images.
-
-# Pass a directory as first parameter to this script.  This will
-# then be used as LOFAR20_DIR.  Otherwise this file's directory
-# be used to determine the tango directory location.
+# This file's directory is used to determine the station control directory
+# location.
 ABSOLUTE_PATH=$(realpath $(dirname ${BASH_SOURCE}))
 export LOFAR20_DIR=${1:-${ABSOLUTE_PATH}}
 
+# Install git post-checkout hook upon next execution of git command
+# git alias eventually automatically uninstalled
 if [ ! -f "${LOFAR20_DIR}/.git/hooks/post-checkout" ]; then
   alias git="cp ${LOFAR20_DIR}/bin/update_submodules.sh ${LOFAR20_DIR}/.git/hooks/post-checkout; cp ${LOFAR20_DIR}/bin/update_submodules.sh ${LOFAR20_DIR}/.git/hooks/post-merge; unalias git; git"
 fi
 
 # This needs to be modified for a development environment.
-# In case you run multiple Docker networks on the same host in parallel, you need to specify a unique
-# network name for each of them.
+# In case you run multiple Docker networks on the same host in parallel,
+# you need to specify a unique network name for each of them.
 export NETWORK_MODE=tangonet
 
-# It is assumed that the Tango host, the computer that runs the TangoDB, is this host.
-# If this is not true, then modify to the Tango host's FQDN and port.
-# Example:  export TANGO_HOST=station-xk25.astron.nl:10000
+# It is assumed that the Tango host, the computer that runs the TangoDB,
+# is this host. If this is not true, then modify to the Tango host's FQDN and
+# port. Example:  export TANGO_HOST=station-xk25.astron.nl:10000
 export TANGO_HOST=$(hostname):10000
 
-#
-# NO MODIFICATION BEYOND THIS POINT!
-#
-
-# Remove all LOFAR1 related environment modifications
-function remove_lofar()
-{
-    tmp=${1//:/ }
-    echo "$(for new in $(for i in ${tmp}; do printf "%s\n" ${i}; done | egrep -v '/opt/lofar/|/opt/WinCC|/opt/stationtest|/opt/operations'); do printf "%s:" ${new}; done)"
-}
-
-unset LOFARROOT
-export PATH=$(remove_lofar ${PATH})
-export LD_LIBRARY_PATH=$(remove_lofar ${LD_LIBRARY_PATH})
-export PYTHON_PATH=$(remove_lofar ${PYTHON_PATH})
-
-
-# Allow read access for everybody to allow Docker the forwarding of X11.
+# Allow read access for everybody to allow Docker forwarding of X11.
 chmod a+r ~/.Xauthority
-
-# Source the LOFAR2.0 Python3 venv if it exists.
-[ -z ${VIRTUAL_ENV} ] && [ -d ${LOFAR20_DIR}/lofar2.0_venv ] && source ${LOFAR20_DIR}/lofar2.0_venv/bin/activate
-
diff --git a/tangostationcontrol/MANIFEST.in b/tangostationcontrol/MANIFEST.in
index 389b2862f97c448983b3c1375ca819f201be1219..4f8b2d03b275d161613f418c6f50d8378462ac4f 100644
--- a/tangostationcontrol/MANIFEST.in
+++ b/tangostationcontrol/MANIFEST.in
@@ -1,3 +1,7 @@
 include LICENSE
 include README.md
 include VERSION
+
+recursive-include docs *
+recursive-exclude tangostationcontrol/test *
+recursive-exclude tangostationcontrol/integration_test *
diff --git a/tangostationcontrol/VERSION b/tangostationcontrol/VERSION
index ac454c6a1fc3f05f60d3772b45f0b1a5db4b9f87..34a83616bb5aa9a70c5713bc45cd45498a50ba24 100644
--- a/tangostationcontrol/VERSION
+++ b/tangostationcontrol/VERSION
@@ -1 +1 @@
-0.12.0
+0.12.1
diff --git a/tangostationcontrol/setup.cfg b/tangostationcontrol/setup.cfg
index 263ae8197e4f527226332aa7fb872398c1ad7c97..5ccd4a8ac711b068f986fd11c257ab5c7eb29380 100644
--- a/tangostationcontrol/setup.cfg
+++ b/tangostationcontrol/setup.cfg
@@ -21,8 +21,7 @@ classifier =
     Programming Language :: Python :: 3.10
 
 [options]
-package_dir =
-    =.
+include_package_data = true
 packages = find:
 python_requires = >3.10
 install_requires =
@@ -44,7 +43,7 @@ console_scripts =
     l2ss-antennafield = tangostationcontrol.devices.antennafield:main
     l2ss-boot = tangostationcontrol.devices.boot:main
     l2ss-station-manager = tangostationcontrol.devices.station_manager:main
-    l2ss-docker = tangostationcontrol.devices.docker_device:main
+    l2ss-docker = tangostationcontrol.devices.docker:main
     l2ss-observation = tangostationcontrol.devices.observation:main
     l2ss-observationcontrol = tangostationcontrol.devices.observation_control:main
     l2ss-recv = tangostationcontrol.devices.recv:main
@@ -54,7 +53,7 @@ console_scripts =
     l2ss-unb2 = tangostationcontrol.devices.unb2:main
     l2ss-xst = tangostationcontrol.devices.sdp.xst:main
     l2ss-temperaturemanager = tangostationcontrol.devices.temperature_manager:main
-    l2ss-configuration = tangostationcontrol.devices.configuration_device:main
+    l2ss-configuration = tangostationcontrol.devices.configuration:main
 
 # The following entry points should eventually be removed / replaced
     l2ss-hardware-device-template = tangostationcontrol.examples.HW_device_template:main
diff --git a/tangostationcontrol/tangostationcontrol/devices/antennafield.py b/tangostationcontrol/tangostationcontrol/devices/antennafield.py
index e34a51568dd7596926779bde271e05bead5a98b6..c6cde673ee2cba4933ccd070ea6f96a3ee9a16e0 100644
--- a/tangostationcontrol/tangostationcontrol/devices/antennafield.py
+++ b/tangostationcontrol/tangostationcontrol/devices/antennafield.py
@@ -55,7 +55,7 @@ from tangostationcontrol.common.lofar_logging import (
 from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES
 from tangostationcontrol.common.type_checking import type_not_sequence
 from tangostationcontrol.devices.device_decorators import fault_on_error, only_in_states
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 from tangostationcontrol.devices.sdp.common import (
     real_imag_to_weights,
 )
diff --git a/tangostationcontrol/tangostationcontrol/devices/apsct.py b/tangostationcontrol/tangostationcontrol/devices/apsct.py
index b172674746ffa8915147f45e3dc506cb09ff92d5..2d67c1e7292d1b54b761d49c32daf0c313324fb3 100644
--- a/tangostationcontrol/tangostationcontrol/devices/apsct.py
+++ b/tangostationcontrol/tangostationcontrol/devices/apsct.py
@@ -19,7 +19,7 @@ from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
 from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES
 from tangostationcontrol.devices.device_decorators import only_in_states
-from tangostationcontrol.devices.opcua_device import OPCUADevice
+from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice
 
 # Additional import
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/apspu.py b/tangostationcontrol/tangostationcontrol/devices/apspu.py
index 24d83f5b9629534b9a390a48e25ca7345c83f7c6..03c19d94bf6d34ef00fb5174754907f28517a408 100644
--- a/tangostationcontrol/tangostationcontrol/devices/apspu.py
+++ b/tangostationcontrol/tangostationcontrol/devices/apspu.py
@@ -14,7 +14,7 @@ from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper
 from tangostationcontrol.common.constants import DEFAULT_POLLING_PERIOD
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
-from tangostationcontrol.devices.opcua_device import OPCUADevice
+from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice
 
 # Additional import
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/boot.py b/tangostationcontrol/tangostationcontrol/devices/boot.py
index 20c8eeb20fb213cb13dbbab08b9e75cfe040c455..6bc05990be595c2d3dcd3f63d1500d80a4136bf2 100644
--- a/tangostationcontrol/tangostationcontrol/devices/boot.py
+++ b/tangostationcontrol/tangostationcontrol/devices/boot.py
@@ -24,7 +24,7 @@ from tangostationcontrol.common.lofar_logging import (
 )
 from tangostationcontrol.common.states import OPERATIONAL_STATES
 from tangostationcontrol.devices.device_decorators import only_in_states
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/ccd.py b/tangostationcontrol/tangostationcontrol/devices/ccd.py
index 2d5993c96e0aa0e637baf1b2031156b89ad188cb..b3d166180b1d2b70a664f0a0e01e7b869c0e4124 100644
--- a/tangostationcontrol/tangostationcontrol/devices/ccd.py
+++ b/tangostationcontrol/tangostationcontrol/devices/ccd.py
@@ -19,7 +19,7 @@ from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
 from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES
 from tangostationcontrol.devices.device_decorators import only_in_states
-from tangostationcontrol.devices.opcua_device import OPCUADevice
+from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice
 
 # Additional import
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/configuration_device.py b/tangostationcontrol/tangostationcontrol/devices/configuration.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/devices/configuration_device.py
rename to tangostationcontrol/tangostationcontrol/devices/configuration.py
index fc0dd1d0c44809812a67d4257f30aa437ab8896b..8fd7451a90bb6301b78dd2a1cf44c3c5995601b5 100644
--- a/tangostationcontrol/tangostationcontrol/devices/configuration_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/configuration.py
@@ -22,7 +22,7 @@ from tangostationcontrol.common.lofar_logging import (
 )
 from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES
 from tangostationcontrol.devices.device_decorators import only_in_states
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/docker_device.py b/tangostationcontrol/tangostationcontrol/devices/docker.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/devices/docker_device.py
rename to tangostationcontrol/tangostationcontrol/devices/docker.py
index 144cf34fc3db116d7f1bbf4773fd2de1b907ed11..3431d31f403aa86152ee3b213e5dba4d1e16a17d 100644
--- a/tangostationcontrol/tangostationcontrol/devices/docker_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/docker.py
@@ -21,7 +21,7 @@ from tangostationcontrol.common.lofar_logging import (
     device_logging_to_python,
     log_exceptions,
 )
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/__init__.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tangostationcontrol/tangostationcontrol/devices/beam_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/beam_device.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/devices/beam_device.py
rename to tangostationcontrol/tangostationcontrol/devices/interfaces/beam_device.py
index b45d9b0e7890892c2f463bea4e22e7fce449fa38..542e7ef13b6540ec2cf5404dc0c5dec5c933ae05 100644
--- a/tangostationcontrol/tangostationcontrol/devices/beam_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/beam_device.py
@@ -45,7 +45,7 @@ from tangostationcontrol.devices.device_decorators import (
     only_in_states,
     fault_on_error,
 )
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 __all__ = ["BeamDevice", "main", "BeamTracker"]
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/clock_hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/clock_hierarchy.py
new file mode 100644
index 0000000000000000000000000000000000000000..76527afeb8108db72ca21966b5f658a6e0e703d2
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/clock_hierarchy.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+"""Clock Hierarchy for PyTango devices"""
+
+from typing import Dict
+from typing import Optional
+
+from tango import DeviceProxy
+
+from tangostationcontrol.devices.interfaces.hierarchy_device import (
+    AbstractHierarchyDevice,
+)
+
+
+class ClockHierarchyDevice(AbstractHierarchyDevice):
+    CLOCK_CHILD_PROPERTY = "clock_children"
+    CLOCK_PARENT_PROPERTY = "clock_parent"
+
+    def init(
+        self,
+        device_name: str,
+        proxies: Optional[Dict[str, DeviceProxy]] = None,
+    ):
+        super().init(
+            device_name, self.CLOCK_CHILD_PROPERTY, self.CLOCK_PARENT_PROPERTY, proxies
+        )
diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/control_hiearchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/control_hiearchy.py
new file mode 100644
index 0000000000000000000000000000000000000000..a48a7b7f3286044af01d721619a0ac12beb90c39
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/control_hiearchy.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+"""Control Hierarchy for PyTango devices"""
+from typing import Dict
+from typing import Optional
+
+from tango import DeviceProxy
+
+from tangostationcontrol.devices.interfaces.hierarchy_device import (
+    AbstractHierarchyDevice,
+)
+
+
+class ControlHierarchyDevice(AbstractHierarchyDevice):
+    CONTROL_CHILD_PROPERTY = "control_children"
+    CONTROL_PARENT_PROPERTY = "control_parent"
+
+    def init(
+        self,
+        device_name: str,
+        proxies: Optional[Dict[str, DeviceProxy]] = None,
+    ):
+        super().init(
+            device_name,
+            self.CONTROL_CHILD_PROPERTY,
+            self.CONTROL_PARENT_PROPERTY,
+            proxies,
+        )
diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py
new file mode 100644
index 0000000000000000000000000000000000000000..a62e8a4e3992291de7110b07882591db621f60cc
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy.py
@@ -0,0 +1,206 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+"""Abstract Hierarchy for PyTango devices"""
+
+from abc import ABC
+from typing import Dict
+from typing import List
+from typing import Union
+from typing import Optional
+
+from tango import DeviceProxy
+from tango import DevState
+
+
+class AbstractHierarchy(ABC):
+    """AbstractHierarchy"""
+
+    children_type = Dict[str, Dict[str, Union[DeviceProxy, "children_type"]]]
+
+    def __init__(
+        self,
+        child_property_name: str,
+        children: List[str] = None,
+        parent: Optional[str] = None,
+        proxies: Optional[Dict[str, DeviceProxy]] = None,
+    ):
+        """Construct the hierarchy and provide protected access
+
+        :param child_property_name: The name of the PyTango property to identify
+                                    children from your own direct (grand)children
+        :param children: Your direct children, if any, can be empty list
+        :param parent: Your direct parent, if any, can be None
+        :param proxies: Pass reference to DeviceProxy cache dictionary, if None
+                        it will be created. This can be used to ensure single
+                        DeviceProxy instances when devices implement _multiple_
+                        hierarchies.
+        """
+
+        self._child_property_name = child_property_name
+        self._children = {}
+        self._parent = None
+
+        # Store proxies internally upon creation and only pass references to
+        # them. Ensures only single instance of DeviceProxy is created per
+        # device.
+        self._proxies = proxies or {}
+
+        if parent:
+            self._parent = DeviceProxy(parent)
+
+        if not children:
+            children = []
+        for child in children:
+            self._children[child] = None
+
+    def parent(self) -> Optional[str]:
+        """Return the parent device name if there is a parent
+
+        :return: The device name of the parent or None
+        """
+
+        if not self._parent:
+            return None
+
+        return self._parent.dev_name()
+
+    def _get_or_create_proxy(self, device: str) -> DeviceProxy:
+        """Create a proxy if it does not yet exist otherwise pass reference
+
+        :param device: full device name
+        :return: Reference to DeviceProxy from internal cache
+        """
+
+        if not self._proxies.get(device):
+            self._proxies[device] = DeviceProxy(device)
+
+        return self._proxies[device]
+
+    def _get_children(self, child: str, depth: int) -> children_type:
+        """Recursively create dict structure of DeviceProxy and children
+
+        Built a depth-first recursive dict structure terminating at
+        :attr:`~depth` by reading the :attr:`~self._child_property` property
+        of each proxy. depth may be set to -1 for indefinite recursion
+
+        Resulting datastructure of format
+            _children_ = {
+                device_string: {
+                    'proxy': DeviceProxy(device_string),
+                    'children': _children_
+                },
+                ...
+            }
+
+        :warning: Makes no attempt to detect cycles in the tree and if they
+                  exist will never terminate and consume infinite memory!
+        :param child: full device name string from the current child
+        :param depth: Maximum depth to recurse to, -1 for indefinite
+        :return: recursive datastructure of proxies and children as described
+        """
+
+        proxy = self._get_or_create_proxy(child)
+
+        # TODO(Corne): Assert if this value changes even if the property
+        #              has become persistent / immutable with the original value
+        #              for the given device. If so resolve potential issues
+        children = proxy.get_property(self._child_property_name)[
+            self._child_property_name
+        ]
+
+        if len(children) == 0 or depth == 0:
+            return {"proxy": proxy, "children": {}}
+
+        # Perform depth-first recursion to build tree of children and their
+        # children
+        proxies = {}
+        for child in children:
+            proxies[child] = self._get_children(child, depth - 1)
+
+        return {"proxy": proxy, "children": proxies}
+
+    def children(self, depth: int = 1) -> children_type:
+        """Retrieve DeviceProxies of children up to depth
+
+        :param depth: Maximum steps of traversing children, -1 for unlimited
+        :raises tango.NonDbDevice: Raised if the child device does not exist in
+                                   the tango database
+        :raises tango.ConnectionFailed: Raised if connecting to the tango
+                                        database failed
+        :raises tango.CommunicationFailed: Raised if communication with the
+                                           tango database failed
+        :raises tango.DevFailed: Raised if the tango database encounters an
+                                 error
+        :return: Dict of DeviceProxies, children and grandchildren up to
+                 :attr:`~depth` of recursive structure _children_ = {
+                    device_string: {
+                        'proxy': DeviceProxy(device_string),
+                        'children': _children_
+                    },
+                    ...
+                 }
+        """
+
+        children = {}
+        for child in self._children.keys():
+            children[child] = self._get_children(child, depth - 1)
+
+        return children
+
+    def _child(
+        self, child_filter: str, children: children_type
+    ) -> Optional[DeviceProxy]:
+        """Recurse :attr:`~children` to find device and return it"""
+
+        for name, data in children.items():
+            if name == child_filter:
+                return data["proxy"]
+
+            if not data["children"]:
+                continue
+
+            result = self._child(child_filter, data["children"])
+            if result:
+                return result
+
+    def child(self, child_filter: str) -> Optional[DeviceProxy]:
+        """Retrieve DeviceProxy of child matching full name :attr:`~filter`
+
+        :param child_filter: Full name of the device to retrieve DeviceProxy for
+        :raises tango.NonDbDevice: Raised if the child device does not exist in
+                                   the tango database
+        :raises tango.ConnectionFailed: Raised if connecting to the tango
+                                        database failed
+        :raises tango.CommunicationFailed: Raised if communication with the
+                                           tango database failed
+        :raises tango.DevFailed: Raised if the tango database encounters an
+                                 error
+        :return: Return DeviceProxy of child device or None if it does not exist
+        """
+        return self._child(child_filter, self.children(depth=-1))
+
+    def read_attribute(self, attribute: str) -> any:
+        """Allow to read attribute from parent without direct access
+
+        :param attribute: The attribute to read from the parent, can be RW
+        :return: The data from the attribute
+        :raises tango.DevFailed: The exception from the DeviceProxy if raised
+        """
+
+        if not self._parent:
+            return None
+
+        return getattr(self._parent, attribute)
+
+    def state(self) -> Optional[DevState]:
+        """Return the state of the parent without direct access
+
+        :return: The state of the parent if there is one
+        :raises tango.DevFailed: The exception from the DeviceProxy if raised
+        """
+
+        if not self._parent:
+            return None
+
+        return self._parent.state()
diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy_device.py
new file mode 100644
index 0000000000000000000000000000000000000000..3868f295507865a69bbec2b70e174e4561352622
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/hierarchy_device.py
@@ -0,0 +1,83 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+"""Abstract Hierarchy Device for PyTango devices"""
+
+from typing import Dict
+from typing import Optional
+import logging
+
+from tango import Database
+from tango import DeviceProxy
+from tango import DevState
+
+from tangostationcontrol.devices.interfaces.hierarchy import AbstractHierarchy
+
+logger = logging.getLogger()
+
+
+class AbstractHierarchyDevice:
+    """AbstractHierarchyDevice wraps AbstractHierarchy
+
+    :warning: Do not actually use ABC to make this an abstract class as it will
+              cause conflicting metaclasses with PyTango Device servers
+
+    See :py:class:`AbstractHierarchy` for implementation details, wrapping done
+    to prevent pollution of PyTango device namespace
+    """
+
+    def __init__(self):
+        self._hierarchy = None
+
+    def init(
+        self,
+        device_name: str,
+        child_property: str,
+        parent_property: str,
+        proxies: Optional[Dict[str, DeviceProxy]] = None,
+    ):
+        """Initialize the hierarchy for the device
+
+        :param device_name: Full name of device inheriting AbstractHierarchyDevice
+        :param child_property: name of the PyTango device property to identify children
+        :param parent_property: Name of the PyTango device property to identify the
+                                parent
+        :param proxies: Optional shared dictionary of device proxies for when device
+                        implements multiple hierarchies
+        :raises tango.ConnectionFailed: Raised if connecting to the tango database
+                                        failed
+        :raises tango.CommunicationFailed: Raised if communication with the
+                                           tango database failed
+        :raises tango.DevFailed: Raised if the tango database encounters an
+                                 error
+        """
+
+        db = Database()
+        children = db.get_device_property(device_name, child_property)[child_property]
+        parent = db.get_device_property(device_name, parent_property)[parent_property]
+        if len(parent) == 0:
+            parent = [None]
+        parent = parent[0]
+
+        if not children and not parent:
+            logger.warning(
+                "Device: %s has empty hierarchy, both %s and %s properties are empty",
+                device_name,
+                child_property,
+                parent_property,
+                exc_info=True,
+            )
+
+        self._hierarchy = AbstractHierarchy(child_property, children, parent, proxies)
+
+    def children(self, depth: int = 1) -> AbstractHierarchy.children_type:
+        return self._hierarchy(depth)
+
+    def child(self, child_filter: str) -> Optional[DeviceProxy]:
+        return self._hierarchy.child(child_filter)
+
+    def read_parent_attribute(self, attribute: str) -> any:
+        return self._hierarchy.read_attribute(attribute)
+
+    def parent_state(self) -> DevState:
+        return self._hierarchy.state()
diff --git a/tangostationcontrol/tangostationcontrol/devices/lofar_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/lofar_device.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/devices/lofar_device.py
rename to tangostationcontrol/tangostationcontrol/devices/interfaces/lofar_device.py
diff --git a/tangostationcontrol/tangostationcontrol/devices/opcua_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/opcua_device.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/devices/opcua_device.py
rename to tangostationcontrol/tangostationcontrol/devices/interfaces/opcua_device.py
index 11123502d477acd1c0a6a5182195addbd8ba4b04..c4e94dea83c1716258ba21c0bce28b602773e73d 100644
--- a/tangostationcontrol/tangostationcontrol/devices/opcua_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/opcua_device.py
@@ -14,7 +14,7 @@ import numpy
 from tango.server import device_property, attribute
 from tangostationcontrol.clients.opcua_client import OPCUAConnection
 from tangostationcontrol.common.lofar_logging import log_exceptions
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 # Additional import
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e0458099acdfda2f23be5f1a885ebd73f502746
--- /dev/null
+++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/power_hierarchy.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+"""Power Hierarchy for PyTango devices"""
+
+from typing import Dict
+from typing import Optional
+
+from tango import DeviceProxy
+
+from tangostationcontrol.devices.interfaces.hierarchy_device import (
+    AbstractHierarchyDevice,
+)
+
+
+class PowerHierarchy(AbstractHierarchyDevice):
+    POWER_CHILD_PROPERTY = "power_children"
+    POWER_PARENT_PROPERTY = "power_parent"
+
+    def init(
+        self,
+        device_name: str,
+        proxies: Optional[Dict[str, DeviceProxy]] = None,
+    ):
+        super().init(
+            device_name, self.POWER_CHILD_PROPERTY, self.POWER_PARENT_PROPERTY, proxies
+        )
diff --git a/tangostationcontrol/tangostationcontrol/devices/snmp_device.py b/tangostationcontrol/tangostationcontrol/devices/interfaces/snmp_device.py
similarity index 97%
rename from tangostationcontrol/tangostationcontrol/devices/snmp_device.py
rename to tangostationcontrol/tangostationcontrol/devices/interfaces/snmp_device.py
index a758f66b781701eaa6c5d0f7a6bc91296be2c74f..8ca5a9f9a434b4c0d8f73b05652fe97fcf1116d4 100644
--- a/tangostationcontrol/tangostationcontrol/devices/snmp_device.py
+++ b/tangostationcontrol/tangostationcontrol/devices/interfaces/snmp_device.py
@@ -18,7 +18,7 @@ from tangostationcontrol.common.lofar_logging import (
 )
 
 # Additional import
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 debug.setLogger(debug.Debug("searcher", "compiler", "borrower", "reader"))
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/observation.py b/tangostationcontrol/tangostationcontrol/devices/observation.py
index 9ca844b2c0fdb0bbed7b68b2375ab1d676a9aa78..e94a99ce6eb8f3cc0f2bdb0b5c4de8e202691d62 100644
--- a/tangostationcontrol/tangostationcontrol/devices/observation.py
+++ b/tangostationcontrol/tangostationcontrol/devices/observation.py
@@ -24,7 +24,7 @@ from tangostationcontrol.configuration import ObservationSettings
 from tangostationcontrol.devices.device_decorators import fault_on_error
 from tangostationcontrol.devices.device_decorators import only_in_states
 from tangostationcontrol.devices.device_decorators import only_when_on
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/observation_control.py b/tangostationcontrol/tangostationcontrol/devices/observation_control.py
index 668e399f0034ca21a0c06ea879f7e93da79f11b8..cd3d23714d5d9da48f43d625fe36ae66bd242192 100644
--- a/tangostationcontrol/tangostationcontrol/devices/observation_control.py
+++ b/tangostationcontrol/tangostationcontrol/devices/observation_control.py
@@ -22,7 +22,7 @@ from tangostationcontrol.common.lofar_logging import (
 )
 from tangostationcontrol.configuration import ObservationSettings
 from tangostationcontrol.devices.device_decorators import only_when_on, fault_on_error
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 from tangostationcontrol.devices.observation import Observation
 
 logger = logging.getLogger()
diff --git a/tangostationcontrol/tangostationcontrol/devices/pcon.py b/tangostationcontrol/tangostationcontrol/devices/pcon.py
index 6e92053b636965f93e7de06363a33a0dcf04d670..05ce01e3eac61fb1f8ff88c116efd70ebd647558 100644
--- a/tangostationcontrol/tangostationcontrol/devices/pcon.py
+++ b/tangostationcontrol/tangostationcontrol/devices/pcon.py
@@ -14,7 +14,7 @@ from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper
 # Additional import
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
-from tangostationcontrol.devices.snmp_device import SNMPDevice
+from tangostationcontrol.devices.interfaces.snmp_device import SNMPDevice
 
 debug.setLogger(debug.Debug("searcher", "compiler", "borrower", "reader"))
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/psoc.py b/tangostationcontrol/tangostationcontrol/devices/psoc.py
index 1fcbe70f1e9dae7577b44a0d47e235c143e3c706..a4a211e4e9cd15be366130c8462f5addf45851b1 100644
--- a/tangostationcontrol/tangostationcontrol/devices/psoc.py
+++ b/tangostationcontrol/tangostationcontrol/devices/psoc.py
@@ -20,7 +20,7 @@ from tangostationcontrol.common.lofar_logging import (
     device_logging_to_python,
     log_exceptions,
 )
-from tangostationcontrol.devices.snmp_device import SNMPDevice
+from tangostationcontrol.devices.interfaces.snmp_device import SNMPDevice
 
 debug.setLogger(debug.Debug("searcher", "compiler", "borrower", "reader"))
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/recv.py b/tangostationcontrol/tangostationcontrol/devices/recv.py
index afbd472178e63938e31590bd77de0a4de19373cf..d2af727a02edf259928ba1701c02cf54101a176a 100644
--- a/tangostationcontrol/tangostationcontrol/devices/recv.py
+++ b/tangostationcontrol/tangostationcontrol/devices/recv.py
@@ -29,7 +29,7 @@ from tangostationcontrol.common.frequency_bands import bands
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
 from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES
 from tangostationcontrol.devices.device_decorators import only_in_states
-from tangostationcontrol.devices.opcua_device import OPCUADevice
+from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
index bd24aeee8a5878b714babdfa914bd08d43d8f4ed..facb0d360798d1be0c0713ecf6b33c1690407c74 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/beamlet.py
@@ -35,7 +35,7 @@ from tangostationcontrol.common.constants import (
 # Additional import
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import log_exceptions
-from tangostationcontrol.devices.opcua_device import OPCUADevice
+from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice
 from tangostationcontrol.devices.sdp.common import (
     phases_to_weights,
 )
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py b/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py
index ebde55278d4f7ce014731b91362e3655c47dc343..00fe2b9ec64f6b10c4259de3449ecb8ed769b247 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/digitalbeam.py
@@ -26,7 +26,7 @@ from tangostationcontrol.common.constants import (
 # Additional import
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import log_exceptions
-from tangostationcontrol.devices.beam_device import BeamDevice
+from tangostationcontrol.devices.interfaces.beam_device import BeamDevice
 from tangostationcontrol.devices.device_decorators import TimeIt
 
 logger = logging.getLogger()
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
index 7de573ff163d6c70ebf4b49dc3eb9d371c3729b4..85dd8efd6b57bded79b9b06e080bb8af8c2a3298 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/sdp.py
@@ -28,7 +28,7 @@ from tangostationcontrol.common.constants import (
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
 from tangostationcontrol.devices.device_decorators import TimeIt
-from tangostationcontrol.devices.opcua_device import OPCUADevice
+from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice
 from tangostationcontrol.devices.sdp.common import subband_frequencies
 
 __all__ = ["SDP", "main"]
diff --git a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py
index f3cc0ab89923fc3c483f379a9ea63abc844fceed..ef278b0647bf71f53eae683452bc22c8ee87e8bf 100644
--- a/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py
+++ b/tangostationcontrol/tangostationcontrol/devices/sdp/statistics.py
@@ -17,7 +17,7 @@ from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper
 from tangostationcontrol.clients.statistics.client import StatisticsClient
 from tangostationcontrol.common.constants import MAX_ETH_FRAME_SIZE
 from tangostationcontrol.common.lofar_logging import log_exceptions
-from tangostationcontrol.devices.opcua_device import OPCUADevice
+from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/station_manager.py b/tangostationcontrol/tangostationcontrol/devices/station_manager.py
index f4d80197c44e0c6cb7fd2d7e5fcd23882efe2425..09a7a3419ec36ef75349819fac9a63d496a5c077 100644
--- a/tangostationcontrol/tangostationcontrol/devices/station_manager.py
+++ b/tangostationcontrol/tangostationcontrol/devices/station_manager.py
@@ -5,6 +5,7 @@
 
 """
 
+from enum import Enum
 import logging
 
 # pytango imports
@@ -14,10 +15,9 @@ from tango import DebugIt
 # Additional import
 from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
-from tangostationcontrol.devices.lofar_device import LOFARDevice
 from tangostationcontrol.common.lofar_logging import log_exceptions
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
-from enum import Enum
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py b/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py
index 35aca48310b97f6b8b0ba9d02cf010d7659b9e3d..b37ab02b58a3faf3932b3be4f2bcb5cf7e639ccf 100644
--- a/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py
+++ b/tangostationcontrol/tangostationcontrol/devices/temperature_manager.py
@@ -26,7 +26,7 @@ from tangostationcontrol.common.lofar_logging import (
     device_logging_to_python,
     log_exceptions,
 )
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/devices/tilebeam.py b/tangostationcontrol/tangostationcontrol/devices/tilebeam.py
index 7236fe1d840d6199e01572af77a7376f9aab6b93..0500ff91a9fafab45e31eb0d5cb90eed06d718c0 100644
--- a/tangostationcontrol/tangostationcontrol/devices/tilebeam.py
+++ b/tangostationcontrol/tangostationcontrol/devices/tilebeam.py
@@ -20,7 +20,7 @@ from tangostationcontrol.common.lofar_logging import (
     device_logging_to_python,
     log_exceptions,
 )
-from tangostationcontrol.devices.beam_device import BeamDevice
+from tangostationcontrol.devices.interfaces.beam_device import BeamDevice
 from tangostationcontrol.devices.device_decorators import TimeIt
 
 logger = logging.getLogger()
diff --git a/tangostationcontrol/tangostationcontrol/devices/unb2.py b/tangostationcontrol/tangostationcontrol/devices/unb2.py
index 2176be920f510caa4493b9b2afcf0dbc83e8817b..5196001ac7eb2ee15d71badd50731ab660c48f52 100644
--- a/tangostationcontrol/tangostationcontrol/devices/unb2.py
+++ b/tangostationcontrol/tangostationcontrol/devices/unb2.py
@@ -22,7 +22,7 @@ from tangostationcontrol.common.entrypoint import entry
 from tangostationcontrol.common.lofar_logging import device_logging_to_python
 from tangostationcontrol.common.states import DEFAULT_COMMAND_STATES
 from tangostationcontrol.devices.device_decorators import only_in_states
-from tangostationcontrol.devices.opcua_device import OPCUADevice
+from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice
 
 # Additional import
 
diff --git a/tangostationcontrol/tangostationcontrol/examples/HW_device_template.py b/tangostationcontrol/tangostationcontrol/examples/HW_device_template.py
index 474584f35011de442ee28c6ccadc9c6548442717..b45f0634dfc69f4196f70b62902ab30bdae2b81b 100644
--- a/tangostationcontrol/tangostationcontrol/examples/HW_device_template.py
+++ b/tangostationcontrol/tangostationcontrol/examples/HW_device_template.py
@@ -5,7 +5,7 @@ import logging
 
 # PyTango imports
 from tango.server import run
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 # Additional import
 
diff --git a/tangostationcontrol/tangostationcontrol/examples/load_from_disk/ini_device.py b/tangostationcontrol/tangostationcontrol/examples/load_from_disk/ini_device.py
index 43f0c238b430fa59c675939dd89fdc0e9318403e..df65d1304e4fd794eb4ddd4c9d1dade724194839 100644
--- a/tangostationcontrol/tangostationcontrol/examples/load_from_disk/ini_device.py
+++ b/tangostationcontrol/tangostationcontrol/examples/load_from_disk/ini_device.py
@@ -12,7 +12,7 @@ from tango.server import run
 
 # Additional import
 from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper
-from tangostationcontrol.devices.lofar_device import LOFARDevice
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 from tangostationcontrol.examples.load_from_disk.ini_client import IniClient
 
 logger = logging.getLogger()
diff --git a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py
index 4b1192ce90fc958cb105662d4dd5ab1155a424ba..9db86f35b0a00d3fd02c8f7b462cc653fc72c4b3 100644
--- a/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py
+++ b/tangostationcontrol/tangostationcontrol/integration_test/default/devices/base.py
@@ -2,7 +2,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 from tango._tango import DevState
-from tangostationcontrol.devices.opcua_device import OPCUADevice
+from tangostationcontrol.devices.interfaces.opcua_device import OPCUADevice
 from tangostationcontrol.integration_test import base
 from tangostationcontrol.integration_test.device_proxy import TestDeviceProxy
 
diff --git a/tangostationcontrol/tangostationcontrol/test/README.md b/tangostationcontrol/tangostationcontrol/test/README.md
index 56b174d88e533f075a6db0710924a3f46c7ebdb5..4fdf3d298f0829f2cdb3bb8ca6cab2a343775479 100644
--- a/tangostationcontrol/tangostationcontrol/test/README.md
+++ b/tangostationcontrol/tangostationcontrol/test/README.md
@@ -1,171 +1,4 @@
-# Unit Testing
+# Test files included in Station Control
 
-Is the procedure of testing individual units of code for fit for use. Often
-this entails isolating specific segments and testing the behavior given a
-certain range of different inputs. Arguably, the most effective forms of unit
-testing  deliberately uses _edge cases_ and makes assumptions about any other
-sections of code that are not currently under test.
-
-First the underlying technologies as well as how they are applied in Tango
-Station Control are discussed. If this seems uninteresting, or you are already
-familiar with these concepts you can skip to
-[Running tox tasks](#running tox tasks) and
-[Debugging unit tests](#debugging-unit-tests).
-
-### Table of Contents:
-
-- [Tox](#tox)
-- [Testing approach](#testing-approach)
-- [Mocking](#mocking)
-- [Running tox tasks](#running-tox-tasks)
-- [Debugging unit tests](#debugging-unit-tests)
-
-## Tox
-
-[Tox](https://tox.readthedocs.io/en/latest/) is a commandline tool to simplify
-running Python tasks such as linting and unit testing. Using a simple
-[configuration file](../tox.ini) it can setup a
-[virtual environment](https://virtualenv.pypa.io/en/latest/) that automatically
-installs dependencies before executing the task. These environments persist
-after the task finishes preventing the excessive downloading and installation
-of dependencies.
-
-The Tox environments in this project are configured to install any dependencies
-listed in [test-requirements.txt](../test-requirements.txt),
-this can also  easily be verified within our [configuration file](../tox.ini).
-
-## Testing approach
-
-For testing [stestr](https://stestr.readthedocs.io/en/latest/) is used this
-tool  has main advantage the utilization of a highly parallelized unit test
-runner that automatically scales to the number of simultaneous threads
-supported by the host processor. Other features include automatic test
-discovery and pattern matching to only execute a subset of tests.
-
-However, stestr is incompatible with using breakpoints
-(through [pdb](https://docs.python.org/3/library/pdb.html)) directly and  will
-simply fail the test upon encountering one. The
-[debugging unit tests](#debugging-unit-tests) section describes how to mitigate
-this and still debug individual unit tests effectively.
-
-All tests can be found in this test folder and all tests must inherit from
-`TestCase`, found in [base.py](base.py). This ensures that any test fixtures run
-before and after tests. These test fixtures allow to restore any global modified
-state, such as those of static variables. It is the task of the developer
-writing the unit test to ensure that any global modification is restored.
-
-When writing unit tests it is best practice to mimic the directory structure of
-the original project inside the test directory. Similarly, copying the filenames
-and adding _test__ to the beginning. Below is an example:
-
-* root
-  * database
-    * database_manager.py
-  * test
-    * base.py
-    * database
-        * test_database_manager.py
-
-## Mocking
-
-Contrary to many other programming languages, it is entirely possible to
-modify **any** function, object, file, import at runtime. This allows for a
-great deal of flexibility but also simplicity in the case of unit tests.
-
-Isolating functions is as simple as mocking any of the classes or functions it
-uses and modifying its return values such as specific behavior can be tested.
-Below is a simple demonstration mocking the return value of a function using the
-mock decorator. For more detailed explanations see
-[the official documentation](https://docs.python.org/3/library/unittest.mock.html).
-
-```python
-from unittest import mock
-
-# We pretend that `our_custom_module` contains a function `special_char`
-import our_custom_module
-
-def function_under_test(self):
-    if our_custom_module.special_char():
-        return 12
-    else:
-        return 0
-
-@mock.patch.object(our_custom_module, "special_char")
-def test_function_under_test(self, m_special_char):
-    """ Test functionality of `function_under_test`
-
-    This mock decorator _temporarily_ overwrites the :py:func:`special_char`
-    function within :py:module:`our_custom_module`. We get access to this
-    mocked object as function argument. Concurrent dependencies of these mock
-    statements are automatically solved by stestr.
-    """
-
-    # Mock the return value of the mocked object to be None
-    m_special_char.return_value = None
-
-    # Assert that function_under_test returns 0 when special_char returns None.
-    self.assertEqual(0, function_under_test())
-
-```
-
-## Running tox tasks
-
-Running tasks defined in Tox might be a little different than what you are used
-to. This is due to the Tango devices running from Docker containers. Typically,
-the dependencies are only available inside Docker and not on the host.
-
-The tasks can thus only be accessed by executing Tox from within a Tango device
-Docker container. A simple interactive Docker exec is enough to access them:
-
-```sh
-docker exec -it device-sdp /bin/bash
-cd /opt/lofar/tango/devices/
-tox
-```
-
-For specific tasks the `-e` parameter can be used, in addition, any arguments
-can be appended after the tasks. These arguments can be interpreted within the
-Tox  configuration file as `{posargs}`. Below are a few examples:
-
-```sh
-# Execute unit tests with Python 3.10 and only execute the test_get_version test
-# from the TestLofarGit class found within util/test_lofar.py
-# Noteworthy, this will also execute test_get_version_tag_dirty due to pattern
-# matching.
-tox -e py310 util.test_lofar.TestLofarGit.test_get_version_tag
-# Execute linting
-tox -e pep8
-```
-
-## Debugging unit tests
-
-Debugging works by utilizing the
-[virtual environment](https://virtualenv.pypa.io/en/latest/)that Tox creates.
-These are placed in the .tox/ directory. Each of these environments carries
-the same name as found in _tox.ini_, these match the names used for `-e`
-arguments
-
-Debugging unit tests is done by inserting the following code segment just before
-where you think issues occur:
-
-```python
-import pdb; pdb.set_trace()
-```
-
-Now as said stestr will catch any breakpoints and reraise them so we need to
-avoid using stestr while debugging. Simply source the virtual environment
-created by tox `source .tox/py37/bin/activate`. You should now see that the
-shell $PS1 prompt is modified to indicate the environment is active.
-
-From here execute `python -m testtools.run` and optionally the specific test
-case as command line argument. These test will not run in parallel but support
-all other features such as autodiscovery, test fixtures and mocking.
-
-Any breakpoint will be triggered and you can use the pdb interface
-(very similar to gdb) to step through the code, modify and print variables.
-
-Afterwards simply execute `deactivate` to deactivate the virtual environment.
-**DO NOT FORGOT TO REMOVE YOUR `pdb.set_trace()` STATEMENTS AFTERWARDS**
-
-The best approach to prevent committing `import pdb; pdb.set_trace()` is to
-ensure that all unit tests succeed beforehand.
+Test code shared across unit and integration tests is placed here so it is
+accessible by both.
diff --git a/tangostationcontrol/tangostationcontrol/test/__init__.py b/tangostationcontrol/tangostationcontrol/test/__init__.py
index 68ddd5cdc3efaa38e853aef337c08beb99c50c4c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/tangostationcontrol/tangostationcontrol/test/__init__.py
+++ b/tangostationcontrol/tangostationcontrol/test/__init__.py
@@ -1,2 +0,0 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/__init__.py b/tangostationcontrol/tangostationcontrol/test/devices/__init__.py
index 68ddd5cdc3efaa38e853aef337c08beb99c50c4c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/__init__.py
+++ b/tangostationcontrol/tangostationcontrol/test/devices/__init__.py
@@ -1,2 +0,0 @@
-# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
-# SPDX-License-Identifier: Apache-2.0
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py
index 10ebef47cd282df6a1c041f74a4bc9b1d4969670..a84d75bf2ff6e28aeb7b648445591cf69a1544c7 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py
+++ b/tangostationcontrol/tangostationcontrol/test/devices/test_observation_base.py
@@ -3,7 +3,6 @@
 
 
 class TestObservationBase:
-    # TODO(Corne): Use this once working on L2SS-774
     VALID_JSON = """
             {
               "observation_id": 12345,
diff --git a/tangostationcontrol/test-requirements.txt b/tangostationcontrol/test-requirements.txt
index 8d6926cbab34c65c5cad61e6bdf8e0bbd30ec530..475e44ba5ab4d31d5c5a7e94f942ede2afb89d70 100644
--- a/tangostationcontrol/test-requirements.txt
+++ b/tangostationcontrol/test-requirements.txt
@@ -25,3 +25,5 @@ timeout-decorator>=0.5.0  # MIT
 xenon>=0.8.0 # MIT
 prometheus_client # Apache-2.0
 python-logstash-async # MIT
+pytest >= 7.0.0 # MIT
+pytest-cov >= 3.0.0 # MIT
diff --git a/tangostationcontrol/test/README.md b/tangostationcontrol/test/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..56b174d88e533f075a6db0710924a3f46c7ebdb5
--- /dev/null
+++ b/tangostationcontrol/test/README.md
@@ -0,0 +1,171 @@
+# Unit Testing
+
+Is the procedure of testing individual units of code for fit for use. Often
+this entails isolating specific segments and testing the behavior given a
+certain range of different inputs. Arguably, the most effective forms of unit
+testing  deliberately uses _edge cases_ and makes assumptions about any other
+sections of code that are not currently under test.
+
+First the underlying technologies as well as how they are applied in Tango
+Station Control are discussed. If this seems uninteresting, or you are already
+familiar with these concepts you can skip to
+[Running tox tasks](#running tox tasks) and
+[Debugging unit tests](#debugging-unit-tests).
+
+### Table of Contents:
+
+- [Tox](#tox)
+- [Testing approach](#testing-approach)
+- [Mocking](#mocking)
+- [Running tox tasks](#running-tox-tasks)
+- [Debugging unit tests](#debugging-unit-tests)
+
+## Tox
+
+[Tox](https://tox.readthedocs.io/en/latest/) is a commandline tool to simplify
+running Python tasks such as linting and unit testing. Using a simple
+[configuration file](../tox.ini) it can setup a
+[virtual environment](https://virtualenv.pypa.io/en/latest/) that automatically
+installs dependencies before executing the task. These environments persist
+after the task finishes preventing the excessive downloading and installation
+of dependencies.
+
+The Tox environments in this project are configured to install any dependencies
+listed in [test-requirements.txt](../test-requirements.txt),
+this can also  easily be verified within our [configuration file](../tox.ini).
+
+## Testing approach
+
+For testing [stestr](https://stestr.readthedocs.io/en/latest/) is used this
+tool  has main advantage the utilization of a highly parallelized unit test
+runner that automatically scales to the number of simultaneous threads
+supported by the host processor. Other features include automatic test
+discovery and pattern matching to only execute a subset of tests.
+
+However, stestr is incompatible with using breakpoints
+(through [pdb](https://docs.python.org/3/library/pdb.html)) directly and  will
+simply fail the test upon encountering one. The
+[debugging unit tests](#debugging-unit-tests) section describes how to mitigate
+this and still debug individual unit tests effectively.
+
+All tests can be found in this test folder and all tests must inherit from
+`TestCase`, found in [base.py](base.py). This ensures that any test fixtures run
+before and after tests. These test fixtures allow to restore any global modified
+state, such as those of static variables. It is the task of the developer
+writing the unit test to ensure that any global modification is restored.
+
+When writing unit tests it is best practice to mimic the directory structure of
+the original project inside the test directory. Similarly, copying the filenames
+and adding _test__ to the beginning. Below is an example:
+
+* root
+  * database
+    * database_manager.py
+  * test
+    * base.py
+    * database
+        * test_database_manager.py
+
+## Mocking
+
+Contrary to many other programming languages, it is entirely possible to
+modify **any** function, object, file, import at runtime. This allows for a
+great deal of flexibility but also simplicity in the case of unit tests.
+
+Isolating functions is as simple as mocking any of the classes or functions it
+uses and modifying its return values such as specific behavior can be tested.
+Below is a simple demonstration mocking the return value of a function using the
+mock decorator. For more detailed explanations see
+[the official documentation](https://docs.python.org/3/library/unittest.mock.html).
+
+```python
+from unittest import mock
+
+# We pretend that `our_custom_module` contains a function `special_char`
+import our_custom_module
+
+def function_under_test(self):
+    if our_custom_module.special_char():
+        return 12
+    else:
+        return 0
+
+@mock.patch.object(our_custom_module, "special_char")
+def test_function_under_test(self, m_special_char):
+    """ Test functionality of `function_under_test`
+
+    This mock decorator _temporarily_ overwrites the :py:func:`special_char`
+    function within :py:module:`our_custom_module`. We get access to this
+    mocked object as function argument. Concurrent dependencies of these mock
+    statements are automatically solved by stestr.
+    """
+
+    # Mock the return value of the mocked object to be None
+    m_special_char.return_value = None
+
+    # Assert that function_under_test returns 0 when special_char returns None.
+    self.assertEqual(0, function_under_test())
+
+```
+
+## Running tox tasks
+
+Running tasks defined in Tox might be a little different than what you are used
+to. This is due to the Tango devices running from Docker containers. Typically,
+the dependencies are only available inside Docker and not on the host.
+
+The tasks can thus only be accessed by executing Tox from within a Tango device
+Docker container. A simple interactive Docker exec is enough to access them:
+
+```sh
+docker exec -it device-sdp /bin/bash
+cd /opt/lofar/tango/devices/
+tox
+```
+
+For specific tasks the `-e` parameter can be used, in addition, any arguments
+can be appended after the tasks. These arguments can be interpreted within the
+Tox  configuration file as `{posargs}`. Below are a few examples:
+
+```sh
+# Execute unit tests with Python 3.10 and only execute the test_get_version test
+# from the TestLofarGit class found within util/test_lofar.py
+# Noteworthy, this will also execute test_get_version_tag_dirty due to pattern
+# matching.
+tox -e py310 util.test_lofar.TestLofarGit.test_get_version_tag
+# Execute linting
+tox -e pep8
+```
+
+## Debugging unit tests
+
+Debugging works by utilizing the
+[virtual environment](https://virtualenv.pypa.io/en/latest/)that Tox creates.
+These are placed in the .tox/ directory. Each of these environments carries
+the same name as found in _tox.ini_, these match the names used for `-e`
+arguments
+
+Debugging unit tests is done by inserting the following code segment just before
+where you think issues occur:
+
+```python
+import pdb; pdb.set_trace()
+```
+
+Now as said stestr will catch any breakpoints and reraise them so we need to
+avoid using stestr while debugging. Simply source the virtual environment
+created by tox `source .tox/py37/bin/activate`. You should now see that the
+shell $PS1 prompt is modified to indicate the environment is active.
+
+From here execute `python -m testtools.run` and optionally the specific test
+case as command line argument. These test will not run in parallel but support
+all other features such as autodiscovery, test fixtures and mocking.
+
+Any breakpoint will be triggered and you can use the pdb interface
+(very similar to gdb) to step through the code, modify and print variables.
+
+Afterwards simply execute `deactivate` to deactivate the virtual environment.
+**DO NOT FORGOT TO REMOVE YOUR `pdb.set_trace()` STATEMENTS AFTERWARDS**
+
+The best approach to prevent committing `import pdb; pdb.set_trace()` is to
+ensure that all unit tests succeed beforehand.
diff --git a/tangostationcontrol/tangostationcontrol/test/beam/__init__.py b/tangostationcontrol/test/__init__.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/beam/__init__.py
rename to tangostationcontrol/test/__init__.py
diff --git a/tangostationcontrol/tangostationcontrol/test/base.py b/tangostationcontrol/test/base.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/base.py
rename to tangostationcontrol/test/base.py
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/__init__.py b/tangostationcontrol/test/beam/__init__.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/clients/__init__.py
rename to tangostationcontrol/test/beam/__init__.py
diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py b/tangostationcontrol/test/beam/test_delays.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/test/beam/test_delays.py
rename to tangostationcontrol/test/beam/test_delays.py
index 4993e5cc6c30d44d7980fe952cc67cb46e65e43c..8fa7161c8c45a59231fbc72c070a9853b940d089 100644
--- a/tangostationcontrol/tangostationcontrol/test/beam/test_delays.py
+++ b/tangostationcontrol/test/beam/test_delays.py
@@ -9,9 +9,11 @@ import casacore
 import mock
 import numpy
 import numpy.testing
+
 from tangostationcontrol.beam.delays import Delays
 from tangostationcontrol.common.constants import MAX_ANTENNA, N_beamlets_ctrl
-from tangostationcontrol.test import base
+
+from test import base
 
 
 class TestDelays(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py b/tangostationcontrol/test/beam/test_geo.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/beam/test_geo.py
rename to tangostationcontrol/test/beam/test_geo.py
index 7f6711a93a0490760a084bc7f7eba20f369ffffc..9b9dcce6ff0af4d217c727472b40a5a02805d6d5 100644
--- a/tangostationcontrol/tangostationcontrol/test/beam/test_geo.py
+++ b/tangostationcontrol/test/beam/test_geo.py
@@ -2,8 +2,10 @@
 # SPDX-License-Identifier: Apache-2.0
 
 import numpy.testing
+
 from tangostationcontrol.beam.geo import ETRS_to_ITRF, ETRS_to_GEO, GEO_to_GEOHASH
-from tangostationcontrol.test import base
+
+from test import base
 
 
 class TestETRSToITRF(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py b/tangostationcontrol/test/beam/test_hba_tile.py
similarity index 97%
rename from tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py
rename to tangostationcontrol/test/beam/test_hba_tile.py
index 3063008a56cfc767b03df0b8aaf80b894dcaf8b4..19ef26793dc8f84f4ae9957d564e8da709eb3751 100644
--- a/tangostationcontrol/tangostationcontrol/test/beam/test_hba_tile.py
+++ b/tangostationcontrol/test/beam/test_hba_tile.py
@@ -4,8 +4,10 @@
 from math import pi
 
 import numpy.testing
+
 from tangostationcontrol.beam.hba_tile import HBATAntennaOffsets
-from tangostationcontrol.test import base
+
+from test import base
 
 
 class TestHBATAntennaOffsets(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/common/__init__.py b/tangostationcontrol/test/clients/__init__.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/common/__init__.py
rename to tangostationcontrol/test/clients/__init__.py
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-CONF.mib b/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-CONF.mib
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-CONF.mib
rename to tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-CONF.mib
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-SMI.mib b/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-SMI.mib
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-SMI.mib
rename to tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-SMI.mib
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-TC.mib b/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-TC.mib
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-TC.mib
rename to tangostationcontrol/test/clients/snmp_mib_loading/SNMPv2-TC.mib
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/TEST-MIB.mib b/tangostationcontrol/test/clients/snmp_mib_loading/TEST-MIB.mib
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/clients/snmp_mib_loading/TEST-MIB.mib
rename to tangostationcontrol/test/clients/snmp_mib_loading/TEST-MIB.mib
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py b/tangostationcontrol/test/clients/test_attr_wrapper.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py
rename to tangostationcontrol/test/clients/test_attr_wrapper.py
index 91226ed88b2396e45826f6512374dc9b5bb202cc..d540d946b3290d1a6f3cadf4a31c7b1edf208dde 100644
--- a/tangostationcontrol/tangostationcontrol/test/clients/test_attr_wrapper.py
+++ b/tangostationcontrol/test/clients/test_attr_wrapper.py
@@ -5,22 +5,22 @@
 """
 
 import asyncio
-
 import mock
-import numpy
-import tangostationcontrol.devices.lofar_device
+
+import tangostationcontrol.devices.interfaces.lofar_device
+from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper
+from tangostationcontrol.devices.interfaces.lofar_device import LOFARDevice
 
 # External imports
+import numpy
 from tango import DevState, DevFailed, AttrWriteType
 
 # Test imports
 from tango.test_context import DeviceTestContext
-from tangostationcontrol.clients.attribute_wrapper import AttributeWrapper
-from tangostationcontrol.devices.lofar_device import LOFARDevice
-from tangostationcontrol.test import base
 
 # Internal imports
-from tangostationcontrol.test.clients.test_client import TestClient
+from test import base
+from test.clients.test_client import TestClient
 
 SCALAR_DIMS = (1,)
 SPECTRUM_DIMS = (4,)
@@ -43,7 +43,7 @@ class TestAttributeTypes(base.TestCase):
     def setUp(self):
         # Avoid the device trying to access itself as a client
         self.deviceproxy_patch = mock.patch.object(
-            tangostationcontrol.devices.lofar_device, "DeviceProxy"
+            tangostationcontrol.devices.interfaces.lofar_device, "DeviceProxy"
         )
         self.deviceproxy_patch.start()
         self.addCleanup(self.deviceproxy_patch.stop)
@@ -1015,7 +1015,7 @@ class TestAttributeAccess(base.TestCase):
     def setUp(self):
         # Avoid the device trying to access itself as a client
         self.deviceproxy_patch = mock.patch.object(
-            tangostationcontrol.devices.lofar_device, "DeviceProxy"
+            tangostationcontrol.devices.interfaces.lofar_device, "DeviceProxy"
         )
         self.deviceproxy_patch.start()
         self.addCleanup(self.deviceproxy_patch.stop)
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_client.py b/tangostationcontrol/test/clients/test_client.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/clients/test_client.py
rename to tangostationcontrol/test/clients/test_client.py
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py b/tangostationcontrol/test/clients/test_opcua_client.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py
rename to tangostationcontrol/test/clients/test_opcua_client.py
index 643312fedac250d9b064d6070c02bbddc5fe0300..fd4e4dae3999d6fdbc38cd0e47278c6cfe8f7573 100644
--- a/tangostationcontrol/tangostationcontrol/test/clients/test_opcua_client.py
+++ b/tangostationcontrol/test/clients/test_opcua_client.py
@@ -8,9 +8,11 @@ from unittest import mock
 import asynctest
 import asyncua
 import numpy
+
 from tangostationcontrol.clients import opcua_client
 from tangostationcontrol.clients.opcua_client import OPCUAConnection
-from tangostationcontrol.test import base
+
+from test import base
 
 
 class AttrProps:
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py b/tangostationcontrol/test/clients/test_snmp_client.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py
rename to tangostationcontrol/test/clients/test_snmp_client.py
index 9f6b895783bf3a1489be88abcec9ec929f16cc2c..296f8c08dc646d3a9d99116cfb0a743c3c94bf4a 100644
--- a/tangostationcontrol/tangostationcontrol/test/clients/test_snmp_client.py
+++ b/tangostationcontrol/test/clients/test_snmp_client.py
@@ -8,11 +8,13 @@ import numpy
 from pysnmp import hlapi
 from pysnmp.smi import view, error
 from pysnmp.smi.rfc1902 import ObjectIdentity
+
 from tangostationcontrol.clients.snmp_client import MIBLoader
 from tangostationcontrol.clients.snmp_client import SNMPAttribute
 from tangostationcontrol.clients.snmp_client import SNMPClient
 from tangostationcontrol.clients.snmp_client import SNMPComm
-from tangostationcontrol.test import base
+
+from test import base
 
 
 class SNMPServerFixture:
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_statistics_client_thread.py b/tangostationcontrol/test/clients/test_statistics_client_thread.py
similarity index 95%
rename from tangostationcontrol/tangostationcontrol/test/clients/test_statistics_client_thread.py
rename to tangostationcontrol/test/clients/test_statistics_client_thread.py
index 6d9f01ef76f122b126747fa6ebfe2e64a1f619d9..cef62b535bad63d8801be288b264ff80c0f5311e 100644
--- a/tangostationcontrol/tangostationcontrol/test/clients/test_statistics_client_thread.py
+++ b/tangostationcontrol/test/clients/test_statistics_client_thread.py
@@ -5,7 +5,8 @@ import logging
 from unittest import mock
 
 from tangostationcontrol.clients.statistics.client_thread import StatisticsClientThread
-from tangostationcontrol.test import base
+
+from test import base
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/test/clients/test_tcp_replicator.py b/tangostationcontrol/test/clients/test_tcp_replicator.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/test/clients/test_tcp_replicator.py
rename to tangostationcontrol/test/clients/test_tcp_replicator.py
index a6e3a24f67728d90969c1c7fab0353a954670303..f51e9973496da7af6b9399243de725337c99e917 100644
--- a/tangostationcontrol/tangostationcontrol/test/clients/test_tcp_replicator.py
+++ b/tangostationcontrol/test/clients/test_tcp_replicator.py
@@ -6,8 +6,10 @@ import logging
 from unittest import mock
 
 import timeout_decorator
+
 from tangostationcontrol.clients.tcp_replicator import TCPReplicator
-from tangostationcontrol.test import base
+
+from test import base
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/__init__.py b/tangostationcontrol/test/common/__init__.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/configuration/__init__.py
rename to tangostationcontrol/test/common/__init__.py
diff --git a/tangostationcontrol/tangostationcontrol/test/common/fake_measures.ztar b/tangostationcontrol/test/common/fake_measures.ztar
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/common/fake_measures.ztar
rename to tangostationcontrol/test/common/fake_measures.ztar
diff --git a/tangostationcontrol/tangostationcontrol/test/common/fake_measures_newer.ztar b/tangostationcontrol/test/common/fake_measures_newer.ztar
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/common/fake_measures_newer.ztar
rename to tangostationcontrol/test/common/fake_measures_newer.ztar
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_baselines.py b/tangostationcontrol/test/common/test_baselines.py
similarity index 96%
rename from tangostationcontrol/tangostationcontrol/test/common/test_baselines.py
rename to tangostationcontrol/test/common/test_baselines.py
index ca1486f27d4da8f83c3fca227947ed22972450c8..eed7d7dda120a57f69885c7a6f9ffa4f503fac09 100644
--- a/tangostationcontrol/tangostationcontrol/test/common/test_baselines.py
+++ b/tangostationcontrol/test/common/test_baselines.py
@@ -4,7 +4,7 @@
 from tangostationcontrol.common import baselines
 from tangostationcontrol.common.constants import MAX_INPUTS
 
-from tangostationcontrol.test import base
+from test import base
 
 
 class TestBaselines(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_cables.py b/tangostationcontrol/test/common/test_cables.py
similarity index 97%
rename from tangostationcontrol/tangostationcontrol/test/common/test_cables.py
rename to tangostationcontrol/test/common/test_cables.py
index 96a78dd89cb684b5e46e31a2edb635f830816afc..0ffc3b55a9e4ea4a7e457db53979034c74d49659 100644
--- a/tangostationcontrol/tangostationcontrol/test/common/test_cables.py
+++ b/tangostationcontrol/test/common/test_cables.py
@@ -3,7 +3,7 @@
 
 from tangostationcontrol.common import cables
 
-from tangostationcontrol.test import base
+from test import base
 
 
 class TestCables(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_calibration.py b/tangostationcontrol/test/common/test_calibration.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/test/common/test_calibration.py
rename to tangostationcontrol/test/common/test_calibration.py
index 6d13f8929dd25dbab41bf0b89ee204ff7bfa3e07..44b87271f5ff535e2fc17b747d446d7be727f86a 100644
--- a/tangostationcontrol/tangostationcontrol/test/common/test_calibration.py
+++ b/tangostationcontrol/test/common/test_calibration.py
@@ -2,12 +2,14 @@
 # SPDX-License-Identifier: Apache-2.0
 
 import numpy
+
 from tangostationcontrol.common.calibration import (
     delay_compensation,
     loss_compensation,
     dB_to_factor,
 )
-from tangostationcontrol.test import base
+
+from test import base
 
 
 class TestCalibration(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_lofar_logging.py b/tangostationcontrol/test/common/test_lofar_logging.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/test/common/test_lofar_logging.py
rename to tangostationcontrol/test/common/test_lofar_logging.py
index daaeb69a8d01e3806730410830dff953d1a41ddc..4af2e720736eb7424bd223c732114917d3c49804 100644
--- a/tangostationcontrol/tangostationcontrol/test/common/test_lofar_logging.py
+++ b/tangostationcontrol/test/common/test_lofar_logging.py
@@ -8,8 +8,10 @@ from unittest import mock
 from tango import device_server, DevFailed
 from tango.server import Device
 from tango.test_context import DeviceTestContext
+
 from tangostationcontrol.common import lofar_logging
-from tangostationcontrol.test import base
+
+from test import base
 
 
 class TestExceptionToStr(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_measures.py b/tangostationcontrol/test/common/test_measures.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/common/test_measures.py
rename to tangostationcontrol/test/common/test_measures.py
index ec8e0369af8451a8cb10f36a7a55433102cfc4ff..808ba0a892184fd86f3da437c74b1d3c3c8b5fc4 100644
--- a/tangostationcontrol/tangostationcontrol/test/common/test_measures.py
+++ b/tangostationcontrol/test/common/test_measures.py
@@ -8,7 +8,8 @@ import urllib.request
 from unittest import mock
 
 from tangostationcontrol.common import measures
-from tangostationcontrol.test import base
+
+from test import base
 
 # where our WSRT_Measures.ztar surrogate is located
 # two versions with different timestamps are provided
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py b/tangostationcontrol/test/common/test_observation_controller.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py
rename to tangostationcontrol/test/common/test_observation_controller.py
index c2d7997c06167b5abfb4beb4aabb228c1f173c02..0de8ae33ba9c8d16eb49c96fc10fbf6380c48a1e 100644
--- a/tangostationcontrol/tangostationcontrol/test/common/test_observation_controller.py
+++ b/tangostationcontrol/test/common/test_observation_controller.py
@@ -8,10 +8,12 @@ from unittest import mock
 from unittest.mock import Mock
 
 from tango import DevState
+
 from tangostationcontrol.common import ObservationController
 from tangostationcontrol.common.observation_controller import RunningObservation
 from tangostationcontrol.configuration import ObservationSettings, Pointing, Sap
-from tangostationcontrol.test import base
+
+from test import base
 
 
 @mock.patch("tango.Util.instance")
diff --git a/tangostationcontrol/tangostationcontrol/test/common/test_type_checking.py b/tangostationcontrol/test/common/test_type_checking.py
similarity index 97%
rename from tangostationcontrol/tangostationcontrol/test/common/test_type_checking.py
rename to tangostationcontrol/test/common/test_type_checking.py
index f5b1816d39e93899f0e4c82b2eb8afac5e3564df..c42b3b00cff92356284fcc40eb37996219cf6c33 100644
--- a/tangostationcontrol/tangostationcontrol/test/common/test_type_checking.py
+++ b/tangostationcontrol/test/common/test_type_checking.py
@@ -3,8 +3,10 @@
 
 import numpy
 from tango.utils import is_seq
+
 from tangostationcontrol.common import type_checking
-from tangostationcontrol.test import base
+
+from test import base
 
 
 class TestTypeChecking(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/prometheus/__init__.py b/tangostationcontrol/test/configuration/__init__.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/prometheus/__init__.py
rename to tangostationcontrol/test/configuration/__init__.py
diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/_mock_requests.py b/tangostationcontrol/test/configuration/_mock_requests.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/configuration/_mock_requests.py
rename to tangostationcontrol/test/configuration/_mock_requests.py
diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py b/tangostationcontrol/test/configuration/test_observation_settings.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py
rename to tangostationcontrol/test/configuration/test_observation_settings.py
index cba385096eff77a90d799f75da18f7f537df6638..7002c9c63962f4e885a62f2be2081f2c606b4532 100644
--- a/tangostationcontrol/tangostationcontrol/test/configuration/test_observation_settings.py
+++ b/tangostationcontrol/test/configuration/test_observation_settings.py
@@ -6,9 +6,11 @@ from unittest import mock
 
 import requests
 from jsonschema.exceptions import ValidationError, RefResolutionError
+
 from tangostationcontrol.configuration import Pointing, ObservationSettings, Sap
-from tangostationcontrol.test import base
-from tangostationcontrol.test.configuration._mock_requests import mocked_requests_get
+
+from test import base
+from test.configuration._mock_requests import mocked_requests_get
 
 
 @mock.patch("requests.get", side_effect=mocked_requests_get)
diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/test_pointing.py b/tangostationcontrol/test/configuration/test_pointing.py
similarity index 93%
rename from tangostationcontrol/tangostationcontrol/test/configuration/test_pointing.py
rename to tangostationcontrol/test/configuration/test_pointing.py
index 0161f0a35bfba492605cb179db7237858286d98d..a5ca5955d508c8bd4744bb0f049ae793f212abeb 100644
--- a/tangostationcontrol/tangostationcontrol/test/configuration/test_pointing.py
+++ b/tangostationcontrol/test/configuration/test_pointing.py
@@ -5,9 +5,11 @@ from unittest import mock
 
 import requests
 from jsonschema.exceptions import ValidationError, RefResolutionError
+
 from tangostationcontrol.configuration import Pointing
-from tangostationcontrol.test import base
-from tangostationcontrol.test.configuration._mock_requests import mocked_requests_get
+
+from test import base
+from test.configuration._mock_requests import mocked_requests_get
 
 
 @mock.patch("requests.get", side_effect=mocked_requests_get)
diff --git a/tangostationcontrol/tangostationcontrol/test/configuration/test_sap_settings.py b/tangostationcontrol/test/configuration/test_sap_settings.py
similarity index 95%
rename from tangostationcontrol/tangostationcontrol/test/configuration/test_sap_settings.py
rename to tangostationcontrol/test/configuration/test_sap_settings.py
index c625f4ebfcaaa2faa80b2736b3584e66fd874e3b..a9afbfd9e80f0db49447bcce5b634882e7a43fd8 100644
--- a/tangostationcontrol/tangostationcontrol/test/configuration/test_sap_settings.py
+++ b/tangostationcontrol/test/configuration/test_sap_settings.py
@@ -5,9 +5,11 @@ from unittest import mock
 
 import requests
 from jsonschema.exceptions import ValidationError, RefResolutionError
+
 from tangostationcontrol.configuration import Pointing, Sap
-from tangostationcontrol.test import base
-from tangostationcontrol.test.configuration._mock_requests import mocked_requests_get
+
+from test import base
+from test.configuration._mock_requests import mocked_requests_get
 
 
 @mock.patch("requests.get", side_effect=mocked_requests_get)
diff --git a/tangostationcontrol/tangostationcontrol/test/statistics/__init__.py b/tangostationcontrol/test/devices/__init__.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/statistics/__init__.py
rename to tangostationcontrol/test/devices/__init__.py
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/Tango_Controls-Automatic_polling_performance_test.md b/tangostationcontrol/test/devices/automatic_polling_performance_test/Tango_Controls-Automatic_polling_performance_test.md
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/Tango_Controls-Automatic_polling_performance_test.md
rename to tangostationcontrol/test/devices/automatic_polling_performance_test/Tango_Controls-Automatic_polling_performance_test.md
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/automatic_polling_performance_test.json b/tangostationcontrol/test/devices/automatic_polling_performance_test/automatic_polling_performance_test.json
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/automatic_polling_performance_test.json
rename to tangostationcontrol/test/devices/automatic_polling_performance_test/automatic_polling_performance_test.json
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/monitoring_performance_test.py b/tangostationcontrol/test/devices/automatic_polling_performance_test/monitoring_performance_test.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/devices/automatic_polling_performance_test/monitoring_performance_test.py
rename to tangostationcontrol/test/devices/automatic_polling_performance_test/monitoring_performance_test.py
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/device_base.py b/tangostationcontrol/test/devices/device_base.py
similarity index 62%
rename from tangostationcontrol/tangostationcontrol/test/devices/device_base.py
rename to tangostationcontrol/test/devices/device_base.py
index 22f8155e20bf3820f3a9ebcadf2e55c13a53fcef..7f782259cd2f5ff2570f29d68f7f20b89bc890e0 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/device_base.py
+++ b/tangostationcontrol/test/devices/device_base.py
@@ -2,8 +2,10 @@
 # SPDX-License-Identifier: Apache-2.0
 
 import mock
-from tangostationcontrol.devices import lofar_device
-from tangostationcontrol.test import base
+
+from tangostationcontrol.devices.interfaces import lofar_device
+
+from test import base
 
 
 class DeviceTestCase(base.TestCase):
@@ -21,7 +23,13 @@ class DeviceTestCase(base.TestCase):
         for device in [lofar_device]:
             self.device_proxy_patch(device)
 
-    def device_proxy_patch(self, device):
-        proxy_patcher = mock.patch.object(device, "DeviceProxy")
+    def device_proxy_patch(self, device_module):
+        """Patch a Python module using DeviceProxy to mock its behavior"""
+
+        proxy_patcher = mock.patch.object(device_module, "DeviceProxy")
         proxy_patcher.start()
         self.addCleanup(proxy_patcher.stop)
+        return {
+            "mock": proxy_patcher,
+            "object": getattr(proxy_patcher.target, proxy_patcher.attribute),
+        }
diff --git a/tangostationcontrol/test/devices/interfaces/__init__.py b/tangostationcontrol/test/devices/interfaces/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py b/tangostationcontrol/test/devices/interfaces/test_beam_device.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py
rename to tangostationcontrol/test/devices/interfaces/test_beam_device.py
index 46c921683f3f8b0d5ab6cbd994aeec023eb499d6..b93d2fa8d6b0a5a5b492765f4eae1ee251ffc60f 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_beam_device.py
+++ b/tangostationcontrol/test/devices/interfaces/test_beam_device.py
@@ -4,21 +4,19 @@
 from typing import Callable
 from typing import Optional
 import datetime
+import logging
 import random
 import statistics
 import time
 
 import numpy as np
 import timeout_decorator
-
 from tango import DevFailed
 
-from tangostationcontrol.test import base
-from tangostationcontrol.test.devices import device_base
+from tangostationcontrol.devices.interfaces.beam_device import BeamTracker
 
-from tangostationcontrol.devices.beam_device import BeamTracker
-
-import logging
+from test import base
+from test.devices import device_base
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/test/devices/interfaces/test_hierarchy.py b/tangostationcontrol/test/devices/interfaces/test_hierarchy.py
new file mode 100644
index 0000000000000000000000000000000000000000..174599a7e884690aa03062a37fe4da67981d4158
--- /dev/null
+++ b/tangostationcontrol/test/devices/interfaces/test_hierarchy.py
@@ -0,0 +1,334 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+
+import copy
+import logging
+from typing import Callable
+from typing import Dict
+from typing import List
+from mock import Mock
+
+from tango import DevState
+
+from tangostationcontrol.devices.interfaces import hierarchy
+
+from test.devices import device_base
+
+logger = logging.getLogger()
+
+
+class TestAbstractHierarchy(device_base.DeviceTestCase):
+    class ConcreteHierarchy(hierarchy.AbstractHierarchy):
+        pass
+
+    TEST_PROPERTY_NAME = "control_children"
+
+    def setUp(self):
+        # DeviceTestCase setUp patches lofar_device DeviceProxy
+        super(TestAbstractHierarchy, self).setUp()
+
+        self.hierarchy_mock = self.device_proxy_patch(hierarchy)
+
+    def test_create_instance(self):
+        """Test default values and if we can create an instance"""
+
+        test_hierarchy = TestAbstractHierarchy.ConcreteHierarchy(
+            self.TEST_PROPERTY_NAME
+        )
+
+        self.assertEqual({}, test_hierarchy._children)
+        self.assertEqual(None, test_hierarchy._parent)
+
+        self.assertEqual({}, test_hierarchy.children())
+        self.assertEqual(None, test_hierarchy.parent())
+
+    def test_get_or_create_proxy_cache(self):
+        """Test if get_or_create_proxy caches without duplicates"""
+
+        test = TestAbstractHierarchy.ConcreteHierarchy(
+            self.TEST_PROPERTY_NAME, children=None, parent=None
+        )
+
+    def test_parent_get_name(self):
+        """Read the name of the parent through mocking DeviceProxy.dev_name"""
+
+        name_station = "stat/stationmanager/1"
+
+        mock_dev_name = Mock()
+        mock_dev_name.dev_name.return_value = name_station
+        self.hierarchy_mock["object"].return_value = mock_dev_name
+
+        test = TestAbstractHierarchy.ConcreteHierarchy(
+            self.TEST_PROPERTY_NAME, children=None, parent=name_station
+        )
+
+        self.assertEqual(name_station, test.parent())
+
+    def test_parent_get_name_no_parent(self):
+        """Read the name of the parent when there is no parent"""
+
+        test = TestAbstractHierarchy.ConcreteHierarchy(
+            self.TEST_PROPERTY_NAME, children=None
+        )
+
+        self.assertEqual(None, test.parent())
+
+    def test_parent_read_attribute(self):
+        """Read an attribute from the parent and get the mocked data"""
+
+        attribute_value = "0.0.1"
+
+        name_station = "stat/stationmanager/1"
+        # Mock an attribute
+        self.hierarchy_mock[
+            "object"
+        ].return_value.FPGA_firmware_version_R = attribute_value
+
+        test = TestAbstractHierarchy.ConcreteHierarchy(
+            self.TEST_PROPERTY_NAME, children=None, parent=name_station
+        )
+
+        self.assertEqual(
+            attribute_value, test.read_attribute("FPGA_firmware_version_R")
+        )
+
+    def test_parent_read_attribute_no_parent(self):
+        """Ensure that read_attribute returns None if there is no parent"""
+
+        test = TestAbstractHierarchy.ConcreteHierarchy(
+            self.TEST_PROPERTY_NAME, children=None
+        )
+
+        self.assertIsNone(test.read_attribute("FPGA_firmware_version_R"))
+
+    def test_parent_get_state(self):
+        """Ensure that we can get parent state"""
+
+        name_station = "stat/stationmanager/1"
+        # Mock the state
+        self.hierarchy_mock["object"].return_value.state.return_value = DevState.FAULT
+
+        test = TestAbstractHierarchy.ConcreteHierarchy(
+            self.TEST_PROPERTY_NAME, children=None, parent=name_station
+        )
+
+        self.assertEqual(DevState.FAULT, test.state())
+
+    def test_parent_get_state_no_parent(self):
+        """Ensure that state returns None if no parent"""
+
+        test = TestAbstractHierarchy.ConcreteHierarchy(
+            self.TEST_PROPERTY_NAME, children=None
+        )
+
+        self.assertIsNone(test.state())
+
+    def children_test_base(
+        self,
+        property_name: str,
+        direct_children: List[str],
+        children_properties: List[Dict[str, List[str]]],
+        fn: Callable[[ConcreteHierarchy], None],
+    ):
+        """Base function for testing children() method
+
+        :param fn: Callable to perform actual tests in
+        """
+
+        test_hierarchy = TestAbstractHierarchy.ConcreteHierarchy(
+            property_name, direct_children, None
+        )
+
+        self.hierarchy_mock[
+            "object"
+        ].return_value.get_property.side_effect = children_properties
+
+        fn(test_hierarchy)
+
+    def test_get_children_depth_1_no_filter(self):
+        """Test finding all children at max depth of 1"""
+
+        depth = 1
+
+        test_children = ["stat/sdp/1", "stat/sdp/2"]
+        test_property_calls = [
+            {self.TEST_PROPERTY_NAME: ["stat/xst/1"]},  # sdp/1
+            {self.TEST_PROPERTY_NAME: ["stat/xstbeam/1"]},  # xst/1
+            {self.TEST_PROPERTY_NAME: []},  # xstbeam,/1
+            {self.TEST_PROPERTY_NAME: []},  # sdp/2
+        ]
+
+        def test_fn(test_hierarchy: TestAbstractHierarchy.ConcreteHierarchy):
+            test = test_hierarchy.children(depth=depth)
+
+            # Test if all keys from test_children are in the children() dict
+            self.assertSetEqual(set(test_children), set(test.keys()))
+
+            # test if for each child there are no further children
+            for child in test_children:
+                self.assertDictEqual({}, test[child]["children"])
+
+        self.children_test_base(
+            self.TEST_PROPERTY_NAME, test_children, test_property_calls, test_fn
+        )
+
+    def test_get_children_depth_2_no_filter(self):
+        """Test recursively finding children limited to depth 2"""
+
+        depth = 2
+
+        test_children = ["stat/sdp/1", "stat/sdp/2"]
+        # Calls to get_property follow depth-first order
+        test_property_calls = [
+            {self.TEST_PROPERTY_NAME: ["stat/antennafield/1"]},  # sdp/1
+            {self.TEST_PROPERTY_NAME: ["stat/tilebeam/1"]},  # antennafield/1
+            {self.TEST_PROPERTY_NAME: []},  # tilebeam/1
+            {self.TEST_PROPERTY_NAME: []},  # sdp/2
+        ]
+
+        def test_fn(test_hierarchy: TestAbstractHierarchy.ConcreteHierarchy):
+            test = test_hierarchy.children(depth=depth)
+
+            # Test that antennafield is included
+            self.assertIsNotNone(
+                test["stat/sdp/1"]["children"].get("stat/antennafield/1")
+            )
+
+            # Test that antennafield child is removed due to depth limit
+            self.assertDictEqual(
+                {}, test["stat/sdp/1"]["children"]["stat/antennafield/1"]["children"]
+            )
+
+        self.children_test_base(
+            self.TEST_PROPERTY_NAME, test_children, test_property_calls, test_fn
+        )
+
+    TEST_CHILDREN_ROOT = ["stat/ccd/1", "stat/psoc/1", "stat/antennafield/1"]
+
+    TEST_CHILDREN_ANTENNAFIELD = [
+        "stat/sdp/1",
+        "stat/tilebeam/1",
+        "stat/digitalbeam/1",
+        "stat/aps/1",
+    ]
+    TEST_CHILDREN_SDP = ["stat/xst/1", "stat/sst/1", "stat/bst/1"]
+    TEST_CHILDREN_APS = ["stat/apsct/1", "stat/apspu/1", "stat/unb2/1", "stat/recv/1"]
+
+    # Calls to get_property follow depth-first order
+    TEST_GET_PROPERTY_CALLS = [
+        {TEST_PROPERTY_NAME: []},  # cdd/1
+        {TEST_PROPERTY_NAME: []},  # psoc/1
+        {TEST_PROPERTY_NAME: TEST_CHILDREN_ANTENNAFIELD},  # antennafield/1
+        {TEST_PROPERTY_NAME: TEST_CHILDREN_SDP},  # sdp/1
+        {TEST_PROPERTY_NAME: []},  # xst/1
+        {TEST_PROPERTY_NAME: []},  # sst/1
+        {TEST_PROPERTY_NAME: []},  # bst/1
+        {TEST_PROPERTY_NAME: []},  # tilebeam/1
+        {TEST_PROPERTY_NAME: ["stat/beamlet/1"]},  # digitalbeam/1
+        {TEST_PROPERTY_NAME: []},  # beamlet/1
+        {TEST_PROPERTY_NAME: TEST_CHILDREN_APS},  # aps/1
+        {TEST_PROPERTY_NAME: []},  # apsct/1
+        {TEST_PROPERTY_NAME: []},  # apspu/1
+        {TEST_PROPERTY_NAME: []},  # unb2/1
+        {TEST_PROPERTY_NAME: []},  # recv/1
+    ]
+
+    def test_get_children_depth_elaborate_no_filter(self):
+        """Create a 3 levels deep hierarchy with ~15 devices"""
+
+        def test_fn(test_hierarchy: TestAbstractHierarchy.ConcreteHierarchy):
+            test = test_hierarchy.children(depth=-1)
+
+            # Test ccd and psoc have no children
+            self.assertDictEqual({}, test["stat/ccd/1"]["children"])
+            self.assertDictEqual({}, test["stat/psoc/1"]["children"])
+
+            # Test antennafield has 4 children
+            self.assertEqual(4, len(test["stat/antennafield/1"]["children"]))
+
+            # Test sdp has 3 children
+            self.assertEqual(
+                3,
+                len(test["stat/antennafield/1"]["children"]["stat/sdp/1"]["children"]),
+            )
+
+            # Test all childs of sdp have no children
+            for sdp_child in self.TEST_CHILDREN_SDP:
+                self.assertDictEqual(
+                    {},
+                    test["stat/antennafield/1"]["children"]["stat/sdp/1"]["children"][
+                        sdp_child
+                    ]["children"],
+                )
+
+            # Test tilebeam has no children
+            self.assertDictEqual(
+                {},
+                test["stat/antennafield/1"]["children"]["stat/tilebeam/1"]["children"],
+            )
+
+            # Test digitalbeam has 1 child
+            self.assertEqual(
+                1,
+                len(
+                    test["stat/antennafield/1"]["children"]["stat/digitalbeam/1"][
+                        "children"
+                    ]
+                ),
+            )
+
+            # Test beamlet has no children
+            self.assertDictEqual(
+                {},
+                test["stat/antennafield/1"]["children"]["stat/digitalbeam/1"][
+                    "children"
+                ]["stat/beamlet/1"]["children"],
+            )
+
+            # Test aps has 4 children
+            self.assertEqual(
+                4,
+                len(test["stat/antennafield/1"]["children"]["stat/aps/1"]["children"]),
+            )
+
+            # Test all childs of aps have no children
+            for aps_child in self.TEST_CHILDREN_APS:
+                self.assertDictEqual(
+                    {},
+                    test["stat/antennafield/1"]["children"]["stat/aps/1"]["children"][
+                        aps_child
+                    ]["children"],
+                )
+
+        self.children_test_base(
+            self.TEST_PROPERTY_NAME,
+            self.TEST_CHILDREN_ROOT,
+            self.TEST_GET_PROPERTY_CALLS,
+            test_fn,
+        )
+
+    def test_get_child_filter(self):
+        """Test we can find every device"""
+
+        test_hierarchy = TestAbstractHierarchy.ConcreteHierarchy(
+            self.TEST_PROPERTY_NAME, self.TEST_CHILDREN_ROOT, None
+        )
+
+        self.hierarchy_mock[
+            "object"
+        ].return_value.get_property.side_effect = self.TEST_GET_PROPERTY_CALLS
+
+        all_children = copy.copy(self.TEST_CHILDREN_ROOT)
+        all_children.extend(self.TEST_CHILDREN_ANTENNAFIELD)
+        all_children.extend(self.TEST_CHILDREN_SDP)
+        all_children.extend(self.TEST_CHILDREN_APS)
+
+        # Find all proxies for each child and match that it is the same
+        # object as cached in `_proxies`
+        for child in all_children:
+            result = test_hierarchy.child(child)
+            self.assertEqual(test_hierarchy._proxies[child], result)
+
+            self.hierarchy_mock[
+                "object"
+            ].return_value.get_property.side_effect = self.TEST_GET_PROPERTY_CALLS
diff --git a/tangostationcontrol/test/devices/interfaces/test_hierarchy_device.py b/tangostationcontrol/test/devices/interfaces/test_hierarchy_device.py
new file mode 100644
index 0000000000000000000000000000000000000000..d99553dc42d36d085bda01ee4083adc95ae1bfd7
--- /dev/null
+++ b/tangostationcontrol/test/devices/interfaces/test_hierarchy_device.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
+import unittest
+
+from tango import DeviceProxy
+from tango.server import command
+from tango.server import device_property
+from tango.test_context import DeviceTestContext
+
+from tangostationcontrol.devices.interfaces import hierarchy
+from tangostationcontrol.devices.interfaces import hierarchy_device
+from tangostationcontrol.devices.interfaces import lofar_device
+
+from test.devices import device_base
+
+
+class TestHierarchyDevice(device_base.DeviceTestCase):
+    def setUp(self):
+        # DeviceTestCase setUp patches lofar_device DeviceProxy
+        super(TestHierarchyDevice, self).setUp()
+
+        self.hierarchy_mock = self.device_proxy_patch(hierarchy)
+
+    @unittest.skip("Mocking required Process=False which is broken in tango")
+    @unittest.mock.patch.object(hierarchy_device, "Database")
+    def test_get_direct_child_device(self, m_database):
+        """Test whether read_attribute really returns the attribute."""
+
+        TEST_PROPERTY_CHILDREN = "hierarchy_children"
+        TEST_PROPERTY_PARENT = "hierarchy_parent"
+        TEST_DEVICE_NAME = "stat/antennafield/1"
+        TEST_CHILD_DEVICE = "stat/sdp/1"
+
+        class MyHierarchyDevice(
+            lofar_device.LOFARDevice, hierarchy_device.AbstractHierarchyDevice
+        ):
+            hierarchy_children = device_property(
+                dtype="DevVarStringArray",
+                mandatory=False,
+                default_value=[TEST_CHILD_DEVICE],
+            )
+
+            @command()
+            def has_child_proxy(self):
+                self.init(
+                    TEST_DEVICE_NAME, TEST_PROPERTY_CHILDREN, TEST_PROPERTY_PARENT
+                )
+
+                if isinstance(self.child(TEST_CHILD_DEVICE), DeviceProxy):
+                    raise Exception
+
+        with DeviceTestContext(MyHierarchyDevice, process=False, timeout=10) as proxy:
+            m_database.return_value.get_device_property.side_effect = [
+                {TEST_PROPERTY_CHILDREN: [TEST_CHILD_DEVICE]},
+                {TEST_PROPERTY_PARENT: []},
+            ]
+
+            self.hierarchy_mock["object"].return_value.get_property.side_effect = [
+                {TEST_PROPERTY_CHILDREN: [TEST_CHILD_DEVICE]},
+                {TEST_PROPERTY_CHILDREN: []},
+            ]
+            proxy.has_child_proxy()
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py b/tangostationcontrol/test/devices/interfaces/test_lofar_device.py
similarity index 97%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py
rename to tangostationcontrol/test/devices/interfaces/test_lofar_device.py
index 9621a930ff980f89d568176263ebfddc54a8c73e..2d11c9018fcc11a4bb5c1eeab98b56e5edc418ec 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_lofar_device.py
+++ b/tangostationcontrol/test/devices/interfaces/test_lofar_device.py
@@ -11,8 +11,10 @@ from tango import DevVarBooleanArray
 from tango.server import attribute
 from tango.server import command
 from tango.test_context import DeviceTestContext
-from tangostationcontrol.devices import lofar_device
-from tangostationcontrol.test.devices import device_base
+
+from tangostationcontrol.devices.interfaces import lofar_device
+
+from test.devices import device_base
 
 
 class TestLofarDevice(device_base.DeviceTestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/random_data.py b/tangostationcontrol/test/devices/random_data.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/devices/random_data.py
rename to tangostationcontrol/test/devices/random_data.py
diff --git a/tangostationcontrol/test/devices/sdp/__init__.py b/tangostationcontrol/test/devices/sdp/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py b/tangostationcontrol/test/devices/sdp/test_beamlet_device.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py
rename to tangostationcontrol/test/devices/sdp/test_beamlet_device.py
index f733dac95841aa5844e329c8fe334729bfbdf5ee..e6a3309dce2986e88306d9b4360fe3d2b4df8591 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_beamlet_device.py
+++ b/tangostationcontrol/test/devices/sdp/test_beamlet_device.py
@@ -3,10 +3,12 @@
 
 import numpy
 import numpy.testing
+
 from tangostationcontrol.common.constants import CLK_200_MHZ
 from tangostationcontrol.devices.sdp.beamlet import Beamlet
 from tangostationcontrol.devices.sdp.common import weight_to_complex
-from tangostationcontrol.test import base
+
+from test import base
 
 
 # unpack into 16-bit complex
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py b/tangostationcontrol/test/devices/sdp/test_digitalbeam_device.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py
rename to tangostationcontrol/test/devices/sdp/test_digitalbeam_device.py
index 8dd96fdfe34025fe428662c9e75237587bd490a6..dac9bd0991031e18c4c2e9b6e64cf92037255860 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_digitalbeam_device.py
+++ b/tangostationcontrol/test/devices/sdp/test_digitalbeam_device.py
@@ -24,7 +24,7 @@ from tangostationcontrol.common.constants import (
 from tangostationcontrol.devices.sdp import digitalbeam
 
 # Internal test imports
-from tangostationcontrol.test.devices import device_base
+from test.devices import device_base
 
 
 class TestDigitalBeamDevice(device_base.DeviceTestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_sdp_common.py b/tangostationcontrol/test/devices/sdp/test_sdp_common.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_sdp_common.py
rename to tangostationcontrol/test/devices/sdp/test_sdp_common.py
index 9ec8d2d68446f780be206d57a2818c72d0d7648f..431a47b7fae830323f9321c655391a74012af5dc 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_sdp_common.py
+++ b/tangostationcontrol/test/devices/sdp/test_sdp_common.py
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 import numpy
+
 from tangostationcontrol.common.constants import CLK_200_MHZ, CLK_160_MHZ
 from tangostationcontrol.devices.sdp.common import (
     phases_to_weights,
@@ -9,7 +10,8 @@ from tangostationcontrol.devices.sdp.common import (
     subband_frequency,
     weight_to_complex,
 )
-from tangostationcontrol.test import base
+
+from test import base
 
 
 class TestSDPCommon(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_device.py b/tangostationcontrol/test/devices/sdp/test_statistics_device.py
similarity index 97%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_statistics_device.py
rename to tangostationcontrol/test/devices/sdp/test_statistics_device.py
index 25815b132504d0541229ac667749c975e4f31dbe..42ebee21fdc47b1c9a50eea32581ae7acab64860 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_statistics_device.py
+++ b/tangostationcontrol/test/devices/sdp/test_statistics_device.py
@@ -6,7 +6,7 @@ import mock
 from tango import server
 from tango.test_context import DeviceTestContext
 
-from tangostationcontrol.test import base
+from test import base
 
 
 class TestStatisticsDevice(base.TestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py b/tangostationcontrol/test/devices/test_antennafield_device.py
similarity index 99%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py
rename to tangostationcontrol/test/devices/test_antennafield_device.py
index 15eed2ec48d984ef0acc64a5913860aba520ce3d..10ac1ccec6d9a6c4afbb192d18bb9ec072af38e3 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_antennafield_device.py
+++ b/tangostationcontrol/test/devices/test_antennafield_device.py
@@ -24,8 +24,8 @@ from tangostationcontrol.devices.antennafield import (
     AntennaQuality,
     AntennaUse,
 )
-from tangostationcontrol.test import base
-from tangostationcontrol.test.devices import device_base
+from test import base
+from test.devices import device_base
 
 logger = logging.getLogger()
 
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py b/tangostationcontrol/test/devices/test_device_temperature_manager.py
similarity index 94%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py
rename to tangostationcontrol/test/devices/test_device_temperature_manager.py
index dc0f004821cfbb9dfefe7ba8b1e59053c3ff52d4..c01e0b9b79ccdf57ffa634168e4ef04b4cc675ea 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_device_temperature_manager.py
+++ b/tangostationcontrol/test/devices/test_device_temperature_manager.py
@@ -4,8 +4,10 @@
 import time
 
 from tango.test_context import DeviceTestContext
+
 from tangostationcontrol.devices import temperature_manager
-from tangostationcontrol.test.devices import device_base
+
+from test.devices import device_base
 
 
 class TestTemperatureManagerDevice(device_base.DeviceTestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_control_device.py b/tangostationcontrol/test/devices/test_observation_control_device.py
similarity index 90%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_observation_control_device.py
rename to tangostationcontrol/test/devices/test_observation_control_device.py
index 1b502783b03ed975e66af64508d4e6b10e48a6b6..7b5c70be28c9bf76920a2e32dabbb081d2b03e40 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_control_device.py
+++ b/tangostationcontrol/test/devices/test_observation_control_device.py
@@ -1,9 +1,10 @@
 # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-from tangostationcontrol.test import base
 from tangostationcontrol.test.devices import test_observation_base
 
+from test import base
+
 
 class TestObservationControlDevice(
     base.TestCase, test_observation_base.TestObservationBase
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_device.py b/tangostationcontrol/test/devices/test_observation_device.py
similarity index 88%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_observation_device.py
rename to tangostationcontrol/test/devices/test_observation_device.py
index 5b98c9286d3e7f410bf01ff4b502c71677a3e868..78392c400fb406fc0d1cdd569eb7c09b97698766 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_observation_device.py
+++ b/tangostationcontrol/test/devices/test_observation_device.py
@@ -1,9 +1,10 @@
 # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-from tangostationcontrol.test.devices import device_base
 from tangostationcontrol.test.devices import test_observation_base
 
+from test.devices import device_base
+
 
 class TestObservationDevice(
     device_base.DeviceTestCase, test_observation_base.TestObservationBase
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_psoc_device.py b/tangostationcontrol/test/devices/test_psoc_device.py
similarity index 93%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_psoc_device.py
rename to tangostationcontrol/test/devices/test_psoc_device.py
index 3433b5ab9c85c3d7df658261d74e4ea760147ce0..aa09074a48eb05a10d8264b70c69b7dc7580181c 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_psoc_device.py
+++ b/tangostationcontrol/test/devices/test_psoc_device.py
@@ -1,7 +1,7 @@
 # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-from tangostationcontrol.test.devices import device_base
+from test.devices import device_base
 
 
 class TestPSOCDevice(device_base.DeviceTestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py b/tangostationcontrol/test/devices/test_recv_device.py
similarity index 96%
rename from tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py
rename to tangostationcontrol/test/devices/test_recv_device.py
index db0796d9e67e6b1bd205f34b8e8908cebf095628..c0336753028ad4dd28113307fcbf9c950e068b0e 100644
--- a/tangostationcontrol/tangostationcontrol/test/devices/test_recv_device.py
+++ b/tangostationcontrol/test/devices/test_recv_device.py
@@ -7,7 +7,8 @@ from tango import DevFailed
 
 from tangostationcontrol.common.constants import N_rcu, N_rcu_inp, N_elements
 from tangostationcontrol.devices import recv
-from tangostationcontrol.test.devices import device_base
+
+from test.devices import device_base
 
 
 class TestRecvDevice(device_base.DeviceTestCase):
diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/__init__.py b/tangostationcontrol/test/prometheus/__init__.py
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/toolkit/__init__.py
rename to tangostationcontrol/test/prometheus/__init__.py
diff --git a/tangostationcontrol/tangostationcontrol/test/prometheus/test_archiver_policy.py b/tangostationcontrol/test/prometheus/test_archiver_policy.py
similarity index 96%
rename from tangostationcontrol/tangostationcontrol/test/prometheus/test_archiver_policy.py
rename to tangostationcontrol/test/prometheus/test_archiver_policy.py
index 468b61ca243652938d24e7017f0e73eeb1a2f3b4..519bdb7f45f35a5a343731737df3c48343030ec0 100644
--- a/tangostationcontrol/tangostationcontrol/test/prometheus/test_archiver_policy.py
+++ b/tangostationcontrol/test/prometheus/test_archiver_policy.py
@@ -5,7 +5,7 @@ import importlib.util
 import os
 import sys
 
-from tangostationcontrol.test import base
+from test import base
 
 module_name = "ArchiverPolicy"
 file_path = os.path.join(
diff --git a/tangostationcontrol/tangostationcontrol/test/statistics/SDP_SST_statistics_packet.bin b/tangostationcontrol/test/statistics/SDP_SST_statistics_packet.bin
similarity index 100%
rename from tangostationcontrol/tangostationcontrol/test/statistics/SDP_SST_statistics_packet.bin
rename to tangostationcontrol/test/statistics/SDP_SST_statistics_packet.bin
diff --git a/tangostationcontrol/test/statistics/__init__.py b/tangostationcontrol/test/statistics/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..68ddd5cdc3efaa38e853aef337c08beb99c50c4c
--- /dev/null
+++ b/tangostationcontrol/test/statistics/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
diff --git a/tangostationcontrol/test/toolkit/__init__.py b/tangostationcontrol/test/toolkit/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..68ddd5cdc3efaa38e853aef337c08beb99c50c4c
--- /dev/null
+++ b/tangostationcontrol/test/toolkit/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
+# SPDX-License-Identifier: Apache-2.0
diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_config_file.py b/tangostationcontrol/test/toolkit/test_archiver_config_file.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_config_file.py
rename to tangostationcontrol/test/toolkit/test_archiver_config_file.py
index 319e183c2f6c75037618ce52a2ce15e68b1bb238..34b8b8d548df6f492a171b76226c98f38e9a52f6 100644
--- a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_config_file.py
+++ b/tangostationcontrol/test/toolkit/test_archiver_config_file.py
@@ -4,7 +4,7 @@
 import json
 
 import pkg_resources
-from tangostationcontrol.test import base
+from test import base
 from tangostationcontrol.toolkit.archiver_configurator import (
     get_global_env_parameters,
     get_parameters_from_attribute,
diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_configurator.py b/tangostationcontrol/test/toolkit/test_archiver_configurator.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_configurator.py
rename to tangostationcontrol/test/toolkit/test_archiver_configurator.py
index 09f0e3b88d5e0421706327162264990f2fad3bf2..f9c9d7b6223c61b264821325d49f02eb9829707c 100644
--- a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_configurator.py
+++ b/tangostationcontrol/test/toolkit/test_archiver_configurator.py
@@ -4,7 +4,7 @@
 import json
 
 import pkg_resources
-from tangostationcontrol.test import base
+from test import base
 from tangostationcontrol.toolkit.archiver_configurator import (
     get_parameters_from_attribute,
     get_global_env_parameters,
diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_util.py b/tangostationcontrol/test/toolkit/test_archiver_util.py
similarity index 98%
rename from tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_util.py
rename to tangostationcontrol/test/toolkit/test_archiver_util.py
index b53401778309a86faaa315890bea15382e264dcb..587ee73ae15d58b7cda42c0a8b95c5ac39872894 100644
--- a/tangostationcontrol/tangostationcontrol/test/toolkit/test_archiver_util.py
+++ b/tangostationcontrol/test/toolkit/test_archiver_util.py
@@ -1,7 +1,7 @@
 # Copyright (C) 2022 ASTRON (Netherlands Institute for Radio Astronomy)
 # SPDX-License-Identifier: Apache-2.0
 
-from tangostationcontrol.test import base
+from test import base
 from tangostationcontrol.toolkit.archiver_util import (
     get_attribute_from_fqdn,
     split_tango_name,
diff --git a/tangostationcontrol/tangostationcontrol/test/toolkit/test_mib_compiler.py b/tangostationcontrol/test/toolkit/test_mib_compiler.py
similarity index 95%
rename from tangostationcontrol/tangostationcontrol/test/toolkit/test_mib_compiler.py
rename to tangostationcontrol/test/toolkit/test_mib_compiler.py
index 36c723c0751d74c83003ef65a30febb231f38e77..3ad5c14115b3252b7a1fce18666f58c47cacc351 100644
--- a/tangostationcontrol/tangostationcontrol/test/toolkit/test_mib_compiler.py
+++ b/tangostationcontrol/test/toolkit/test_mib_compiler.py
@@ -7,7 +7,7 @@ from os.path import isfile
 from tempfile import TemporaryDirectory
 from unittest import mock
 
-from tangostationcontrol.test import base
+from test import base
 from tangostationcontrol.toolkit.mib_compiler import mib_compiler
 
 
diff --git a/tangostationcontrol/tox.ini b/tangostationcontrol/tox.ini
index 1e04149a8967d99be2aaad6f6d75f53cf656da59..4c335182cef1065cc22df3b63375ab65db1f5236 100644
--- a/tangostationcontrol/tox.ini
+++ b/tangostationcontrol/tox.ini
@@ -1,10 +1,13 @@
 [tox]
-minversion = 3.20
+min_version = 4.3.3
+requires =
+    tox-ignore-env-name-mismatch ~= 0.2.0
 envlist = black,pep8,pylint,py310,docs
-skipsdist = True
 
 [testenv]
 usedevelop = True
+package = wheel
+wheel_build_env = .pkg
 ; Python and tox variables are used to access modules and binaries instead of
 ; directly. This makes the setup robust for using sitepackages=True.
 install_command = {envbindir}/pip3 install {opts} {packages}
@@ -24,8 +27,8 @@ commands_pre =
     {envpython} --version
     pip install --no-cache 'PyTango>=9.3.6,<9.4.0'
 commands =
-    {envpython} -m stestr --version
-    {envpython} -m stestr run {posargs}
+    {envpython} -m pytest --version
+    {envpython} -m pytest test/{posargs}
 
 ; We can't detect the current Python version for an environment dynamically
 ; so each Python version specific job needs its own envdir.
@@ -50,24 +53,19 @@ commands =
     {envpython} -m coverage report --omit='*test*'
 
 [testenv:{cover,coverage}]
-; stestr does not natively support generating coverage reports use
-; `PYTHON=python -m coverage run....` to overcome this.
+runner = ignore_env_name_mismatch
+envdir = {toxworkdir}/coverage
 setenv =
     VIRTUAL_ENV={envdir}
-    PYTHON={envpython} -m coverage run --source tangostationcontrol --parallel-mode
 commands =
-    {envpython} -m stestr --version
-    {envpython} -m coverage --version
-    {envpython} -m coverage erase
-    {envpython} -m stestr run {posargs}
-    {envpython} -m coverage combine
-    {envpython} -m coverage html --omit='*test*' -d cover
-    {envpython} -m coverage xml -o coverage.xml
-    {envpython} -m coverage report --omit='*test*'
+    {envpython} --version
+    {envpython} -m pytest --version
+    {envpython} -m pytest test --cov-report xml --cov-report html --cov-report xml:coverage.xml --cov=test
 
 # Use generative name and command prefixes to reuse the same virtualenv
 # for all linting jobs.
 [testenv:{pep8,black,pylint,format}]
+runner = ignore_env_name_mismatch
 usedevelop = False
 envdir = {toxworkdir}/linting
 commands =
@@ -104,6 +102,7 @@ commands = {envpython} -m build
 [testenv:docs]
 envdir = {toxworkdir}/docs
 deps =
+    -r{toxinidir}/requirements.txt
     -r{toxinidir}/docs/docs-requirements.txt
 commands =
     sphinx-build --version