diff --git a/.gitignore b/.gitignore
index 7d770d82fca276ee7c371b822e2bf639eb92a4c1..4c7f7e8cb91318300efc16acf0376575bdad03ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,11 @@ venv
 # Packaging generations
 *.egg-info
 dist
+build
+
+# Setuptools SCM
+lofar_station_client/_version.py
+
 
 # Caches
 __pycache__
diff --git a/README.md b/README.md
index 78556d4399c9ef0575bd87df6a66f153570ed114..2332056070cf351ef16ad136741797c33b4f941b 100644
--- a/README.md
+++ b/README.md
@@ -129,7 +129,9 @@ tox -e debug path.to.test
 tox -e debug tests.requests.test_prometheus
 ```
 
-## Releasenotes
+## Release notes
+
+- 0.18.0 - MultiStationObservation and StationFutures allow multi field observations
 - 0.17.3 - Fix hosts and ports to be compatible with consul overlay network
 - 0.17.2 - Fix antennafield_device naming after separation in `AFL` and `AFH`
 - 0.17.1 - Add missing `subbands` field to statistics data
diff --git a/docs/cleanup.sh b/docs/cleanup.sh
new file mode 100755
index 0000000000000000000000000000000000000000..aac4cef22a134a44b69e99369755a58507b3794f
--- /dev/null
+++ b/docs/cleanup.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+FILE_DIR=$(dirname -- "$(readlink -f -- "${0}")")
+
+echo "Cleaning.. ${FILE_DIR}/source/source_documentation/*"
+
+for f in "${FILE_DIR}"/source/source_documentation/*
+do
+
+  case $f in
+    */index.rst) true;;
+    *) echo "Removing.. ${f}"; rm "${f}";;
+  esac
+done
diff --git a/lofar_station_client/observation/constants.py b/lofar_station_client/observation/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac8da6b0dd9338b25fdaaf32259ee90ffcf00349
--- /dev/null
+++ b/lofar_station_client/observation/constants.py
@@ -0,0 +1,8 @@
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+"""Constants used across observation management functionality"""
+
+OBSERVATION_CONTROL_DEVICE_NAME = "STAT/ObservationControl/1"
+
+OBSERVATION_FIELD_DEVICE_NAME = "STAT/ObservationField"
diff --git a/lofar_station_client/observation/multi_station_observation.py b/lofar_station_client/observation/multi_station_observation.py
index 09305c818d1f06cf11e51e04561fb2dbd6186b21..1870e874d96f285b8a6425eceb1b7c674cbec0c1 100644
--- a/lofar_station_client/observation/multi_station_observation.py
+++ b/lofar_station_client/observation/multi_station_observation.py
@@ -1,9 +1,5 @@
-# -*- coding: utf-8 -*-
-#
-# This file is part of the LOFAR2.0 lofar-station-client project.
-#
-# Distributed under the terms of the APACHE license.
-# See LICENSE.txt for more info.
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
 """
 Contains class for pythonic interface with observations
@@ -12,7 +8,9 @@ Contains class for pythonic interface with observations
 import logging
 import concurrent.futures
 
-from lofar_station_client.observation.station_futures import StationFutures
+from lofar_station_client.observation.station_observation_future import (
+    StationObservationFuture,
+)
 
 logger = logging.getLogger()
 
@@ -20,20 +18,54 @@ logger = logging.getLogger()
 class MultiStationObservation:
     """
     This class provides a pythonic interface
-    to start an observation on multiple _stations.
+    to start an observation on multiple stations.
     """
 
     TIMEOUT = 10
 
-    def __init__(self, specification: dict, hosts: list):
-        # convert specification dict to json
-        self._specification = specification
-
-        # get the ID
-        self.observation_id = specification["observation_id"]
+    def __init__(self, specifications: dict, hosts: list):
+        """
+
+        :param specifications: l2stationspecs as retrieved from TMSS in format:
+            ``
+            {
+                "stations": [
+                    {
+                       "station": "cs002"
+                       "antenna_fields": [
+                           {
+                              "observation_id": 144345,
+                              "antenna_field": "HBA0",
+                              "antenna_set": "all",
+                           }
+                        }
+                     }
+                ]
+            }
+            ``
+        :param hosts: list of pytango database host:port strings
+        """
+
+        self._specifications = specifications
+
+        if len(specifications["stations"]) != len(hosts):
+            raise ValueError(
+                "Should have a unique host per station in spec!, "
+                f"{len(specifications['stations'])} != {len(hosts)}"
+            )
+
+        # get the ID, assumed to be unique across stations and antenna field
+        self.observation_id = specifications["stations"][0]["antenna_fields"][0][
+            "observation_id"
+        ]
+
+        # TODO(Corne): Check obs id across stations and antenna fields, enforce that it
+        #              is the same id.
 
         # The list of _stations this class controls
-        self._stations = [StationFutures(self._specification, host) for host in hosts]
+        self._stations = []
+        for index, specification in enumerate(self._specifications["stations"]):
+            self._stations.append(StationObservationFuture(specification, hosts[index]))
 
     def _get_results(self, futures):
         # pylint: disable=W0703, C0103
@@ -64,7 +96,8 @@ class MultiStationObservation:
 
         return results
 
-    def _check_success(self, results):
+    @staticmethod
+    def _check_success(results):
         """
         This function returns a bool list for all the command results
         it gives a True for every command that succeeded and a
@@ -82,9 +115,7 @@ class MultiStationObservation:
         return success_list
 
     def start(self):
-        """
-        Start the observation or all hosts
-        """
+        """Start the observation or all hosts"""
 
         # send out the command to start the observation to all _stations at once
         commands = [station.start() for station in self._stations]
diff --git a/lofar_station_client/observation/station_futures.py b/lofar_station_client/observation/station_futures.py
deleted file mode 100644
index e00103a95e1953230d8a98e0ce0b75191b3ae27e..0000000000000000000000000000000000000000
--- a/lofar_station_client/observation/station_futures.py
+++ /dev/null
@@ -1,103 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# This file is part of the LOFAR2.0 lofar-station-client project.
-#
-# Distributed under the terms of the APACHE license.
-# See LICENSE.txt for more info.
-
-"""
-Internal class for station states with regards to observations
-"""
-
-import concurrent.futures
-from json import dumps
-import logging
-
-from tango import DeviceProxy, GreenMode, DevFailed
-
-
-logger = logging.getLogger()
-
-
-class StationFutures:
-    """
-    Container class station and observation data and acccess
-    """
-
-    def __init__(self, specification: dict, host: str):
-        # pylint: disable=C0103
-
-        # store general data
-        self._id = specification["observation_id"]
-        self._host = host
-        self._json_specification = dumps(specification)
-
-        try:
-            # connects to the tangoDb and get the proxy
-            self._control_proxy = DeviceProxy(
-                f"tango://{host}/STAT/ObservationControl/1"
-            )
-            # gives an exception when it fails to ping the proxy
-            _ = self._control_proxy.ping()
-
-            # set station to green mode
-            self._control_proxy.set_green_mode(GreenMode.Futures)
-
-        except DevFailed as e:
-            self._control_proxy = None
-
-            logger.warning(
-                "Failed to connect to device on host %s: %s: %s",
-                host,
-                e.__class__.__name__,
-                e,
-            )
-
-    def _failed_future(self):
-        future = concurrent.futures.Future()
-        future.set_exception(Exception("Station not connected"))
-        return future
-
-    def start(self) -> concurrent.futures:
-        """Start the observation with the given specification on this station"""
-        if not self.connected:
-            return self._failed_future()
-
-        return self._control_proxy.start_observation(
-            self._json_specification, wait=False
-        )
-
-    def stop(self) -> concurrent.futures:
-        """Stop the observation with the given ID on this station"""
-        if not self.connected:
-            return self._failed_future()
-
-        return self._control_proxy.stop_observation(self._id, wait=False)
-
-    @property
-    def is_running(self) -> concurrent.futures:
-        """get whether the observation with the given ID is running on this station"""
-        if not self.connected:
-            return self._failed_future()
-
-        return self._control_proxy.is_observation_running(self._id, wait=False)
-
-    @property
-    def connected(self) -> bool:
-        """get the connection status of this station"""
-        return self._control_proxy is not None
-
-    @property
-    def host(self) -> str:
-        """get the host name of this station"""
-        return self._host
-
-    @property
-    def control_proxy(self) -> DeviceProxy:
-        """get the control proxy of this station"""
-        return self._control_proxy
-
-    @property
-    def observation_proxy(self) -> DeviceProxy:
-        """get the observation proxy of this station"""
-        return DeviceProxy(f"tango://{self.host}/STAT/observation/{self._id}")
diff --git a/lofar_station_client/observation/station_observation.py b/lofar_station_client/observation/station_observation.py
index 30678f962c20c4d610d0a79edd888b8ebaa0a310..abd4a55d72c719e51392c1186959955a34f364a5 100644
--- a/lofar_station_client/observation/station_observation.py
+++ b/lofar_station_client/observation/station_observation.py
@@ -1,9 +1,5 @@
-# -*- coding: utf-8 -*-
-#
-# This file is part of the LOFAR2.0 lofar-station-client project.
-#
-# Distributed under the terms of the APACHE license.
-# See LICENSE.txt for more info.
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
 """
 Contains class for pythonic interface with observations
@@ -11,15 +7,17 @@ Contains class for pythonic interface with observations
 
 import logging
 
-from lofar_station_client.observation.station_futures import StationFutures
+from lofar_station_client.observation.station_observation_future import (
+    StationObservationFuture,
+)
 
 logger = logging.getLogger()
 
 
 class StationObservation:
     """
-    This class provides a pythonic interface to the
-    ObservationControl and Observation devices on a single station.
+    This class provides a blocking pythonic interface to the
+    ObservationControl and ObservationField devices on a single station.
     """
 
     TIMEOUT = 10
@@ -29,7 +27,7 @@ class StationObservation:
         specification: dict,
         host: str = "databaseds.tangonet:10000",
     ):
-        self.station = StationFutures(specification, host)
+        self.station = StationObservationFuture(specification, host)
 
         if not self.station.connected:
             raise RuntimeError(f"Was not able to connect with {host}")
diff --git a/lofar_station_client/observation/station_observation_future.py b/lofar_station_client/observation/station_observation_future.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3364b4c2faf71a6dee5299751a6d055fb267476
--- /dev/null
+++ b/lofar_station_client/observation/station_observation_future.py
@@ -0,0 +1,145 @@
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+"""
+Internal class for station states to observations
+"""
+
+import concurrent.futures
+from json import dumps
+import logging
+from typing import List
+
+from tango import DeviceProxy, GreenMode, DevFailed
+
+from lofar_station_client.observation.constants import (
+    OBSERVATION_CONTROL_DEVICE_NAME,
+    OBSERVATION_FIELD_DEVICE_NAME,
+)
+
+logger = logging.getLogger()
+
+
+class StationObservationFuture:
+
+    """
+    Container class to manage an observation on a specific station using Python Futures
+    Use :py:class:`StationObservation` as wrapper for synchronized blocking interface.
+    """
+
+    def __init__(self, specification: dict, host: str):
+        """Construct object to manage observation on a station using Futures
+
+        :param specification: Specification from TMSS (LOBSTER) should be of pattern:
+            ``{
+               "station": "cs002"
+               "antenna_fields": [
+                   {
+                      "observation_id": 144345,
+                      "antenna_field": "HBA0",
+                      "antenna_set": "all",
+                   }
+                }
+            }``
+        :param host: Tango database host including port
+        """
+        # pylint: disable=C0103
+
+        # Make no attempt to check each field has the same id
+        self._host: str = host
+        self._specification: dict = specification
+        self._id = specification["antenna_fields"][0]["observation_id"]
+        self._observation_field_proxies = None
+
+        self._antenna_fields = []
+        for observation_field in specification["antenna_fields"]:
+            self._antenna_fields.append(observation_field["antenna_field"])
+
+        try:
+            # connects to the tangoDb and get the proxy
+            self._control_proxy = DeviceProxy(
+                f"tango://{self.host}/{OBSERVATION_CONTROL_DEVICE_NAME}"
+            )
+            # gives an exception when it fails to ping the proxy
+            _ = self._control_proxy.ping()
+
+            # set station to green mode
+            self._control_proxy.set_green_mode(GreenMode.Futures)
+
+        except DevFailed as e:
+            self._control_proxy = None
+
+            logger.warning(
+                "Failed to connect to device on host %s: %s: %s",
+                host,
+                e.__class__.__name__,
+                e,
+            )
+
+    @staticmethod
+    def _failed_future():
+        future = concurrent.futures.Future()
+        future.set_exception(Exception("Station not connected"))
+        return future
+
+    def start(self) -> concurrent.futures:
+        """Start the observation with the given specification on this station"""
+        if not self.connected:
+            return self._failed_future()
+
+        return self._control_proxy.add_observation(
+            dumps(self._specification), wait=False
+        )
+
+    def stop(self) -> concurrent.futures:
+        """Stop the observation with the given ID on this station"""
+        if not self.connected:
+            return self._failed_future()
+
+        return self._control_proxy.stop_observation(self._id, wait=False)
+
+    @property
+    def is_running(self) -> concurrent.futures:
+        """get whether the observation with the given ID is running on this station"""
+        if not self.connected:
+            return self._failed_future()
+
+        return self._control_proxy.is_observation_running(self._id, wait=False)
+
+    @property
+    def connected(self) -> bool:
+        """get the connection status of this station"""
+        # TODO(Corne): What if connection fails between construction of object and call?
+        return self._control_proxy is not None
+
+    @property
+    def host(self) -> str:
+        """get the host name of this station"""
+        return self._host
+
+    @property
+    def control_proxy(self) -> DeviceProxy:
+        """get the control proxy of this station"""
+        return self._control_proxy
+
+    @property
+    def observation_field_proxies(self) -> List[DeviceProxy]:
+        """get the observationfield proxies of this station for the given observation"""
+
+        if not self._observation_field_proxies:
+            self._observation_field_proxies = []
+            try:
+                for observation_field in self._specification["antenna_fields"]:
+                    antenna_field = observation_field["antenna_field"]
+                    self._antenna_fields.append(antenna_field)
+                    self._observation_field_proxies.append(
+                        DeviceProxy(
+                            f"tango://{self.host}/{OBSERVATION_FIELD_DEVICE_NAME}/"
+                            f"{self._id}-{antenna_field}"
+                        )
+                    )
+            except DevFailed as ex:
+                self._observation_field_proxies = None
+                raise ex
+
+        return self._observation_field_proxies
diff --git a/pyproject.toml b/pyproject.toml
index 63f09a96268a6e5f9e8829c758d9e208a1b8bb97..03aa6efbad97d4a46c5266e36a2812e83ca519b2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,13 @@
 [build-system]
-requires = ['setuptools>=62.6', 'wheel']
-build-backend = 'setuptools.build_meta'
+requires = [
+    "setuptools>=62.6",
+    "setuptools_scm[toml]>=6.2",
+    "wheel"
+]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+write_to = "lofar_station_client/_version.py"
+
+[tool.pylint]
+ignore = "_version.py"
diff --git a/setup.cfg b/setup.cfg
index 124329ff048a8d76a61b827828ea3dcf5e86d477..f9512d75824240229d9eccfaa324ff6ae49ad0d5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,5 @@
 [metadata]
 name = lofar-station-client
-version = file: VERSION
 description = Client library for using Tango Station Control
 author="Mevius"
 author_email="mol@astron.nl"
@@ -24,6 +23,7 @@ classifiers =
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
+    Programming Language :: Python :: 3.11
     Topic :: Scientific/Engineering
     Topic :: Scientific/Engineering :: Astronomy
 
@@ -42,4 +42,4 @@ console_scripts =
 max-line-length = 88
 extend-ignore = E203
 filename = *.py
-exclude=venv,.tox,.egg-info
+exclude=build,.venv,.git,.tox,dist,docs,*lib/python*,*egg,_version.py
diff --git a/test-requirements.txt b/test-requirements.txt
index dee140df690a310e03ced8a2823e33949ebf9007..cd5b67b0a61f871e8b00bcab2a43c12ef05f1c6e 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -10,3 +10,6 @@ asynctest>=0.13.0 # Apache-2.0
 testscenarios>=0.5.0 # Apache-2.0/BSD
 pytz>=2022.6 # MIT
 psutil>=5.9.4 # BSD
+pytest>=7.3.0 # MIT
+pytest-forked>=1.6.0 # MIT
+pytest-cov >= 3.0.0 # MIT
diff --git a/tests/observation/specs.py b/tests/observation/specs.py
new file mode 100644
index 0000000000000000000000000000000000000000..c098625c9347855076d034613c778571763d708d
--- /dev/null
+++ b/tests/observation/specs.py
@@ -0,0 +1,112 @@
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+from json import loads
+
+SPEC_SINGLE_HOST_SINGLE_FIELD_DICT = loads(
+    """
+        {
+            "stations": [{
+                "station": "cs002",
+                "antenna_fields": [{
+                    "observation_id": 12345,
+                    "stop_time": "2106-02-07T00:00:00",
+                    "antenna_field": "HBA0",
+                    "antenna_set": "ALL",
+                    "antenna_mask": [0,1,2,9],
+                    "filter": "HBA_110_190",
+                    "SAPs": [{
+                        "subbands": [10, 20, 30],
+                        "pointing": {
+                            "angle1": 1.5, "angle2": 0, "direction_type": "J2000"
+                        }
+                    }],
+                    "tile_beam":
+                    { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" },
+                    "first_beamlet": 0
+                }]
+            }]
+        }
+    """
+)
+
+SPEC_TWO_HOST_TWO_FIELD_DICT = loads(
+    """
+        {
+            "stations": [{
+                "station": "cs002",
+                "antenna_fields": [{
+                    "observation_id": 12345,
+                    "stop_time": "2106-02-07T00:00:00",
+                    "antenna_field": "HBA0",
+                    "antenna_set": "ALL",
+                    "antenna_mask": [0,1,2,9],
+                    "filter": "HBA_110_190",
+                    "SAPs": [{
+                        "subbands": [10, 20, 30],
+                        "pointing": {
+                            "angle1": 1.5, "angle2": 0, "direction_type": "J2000"
+                        }
+                    }],
+                    "tile_beam":
+                    { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" },
+                    "first_beamlet": 0
+                },
+                {
+                    "observation_id": 12345,
+                    "stop_time": "2106-02-07T00:00:00",
+                    "antenna_field": "HBA0",
+                    "antenna_set": "ALL",
+                    "antenna_mask": [0,1,2,9],
+                    "filter": "HBA_110_190",
+                    "SAPs": [{
+                        "subbands": [10, 20, 30],
+                        "pointing": {
+                            "angle1": 1.5, "angle2": 0, "direction_type": "J2000"
+                        }
+                    }],
+                    "tile_beam":
+                    { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" },
+                    "first_beamlet": 0
+                }]
+            },
+            {
+                "station": "cs003",
+                "antenna_fields": [{
+                    "observation_id": 12345,
+                    "stop_time": "2106-02-07T00:00:00",
+                    "antenna_field": "HBA0",
+                    "antenna_set": "ALL",
+                    "antenna_mask": [0,1,2,9],
+                    "filter": "HBA_110_190",
+                    "SAPs": [{
+                        "subbands": [10, 20, 30],
+                        "pointing": {
+                            "angle1": 1.5, "angle2": 0, "direction_type": "J2000"
+                        }
+                    }],
+                    "tile_beam":
+                    { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" },
+                    "first_beamlet": 0
+                },
+                {
+                    "observation_id": 12345,
+                    "stop_time": "2106-02-07T00:00:00",
+                    "antenna_field": "HBA1",
+                    "antenna_set": "ALL",
+                    "antenna_mask": [0,1,2,9],
+                    "filter": "HBA_110_190",
+                    "SAPs": [{
+                        "subbands": [10, 20, 30],
+                        "pointing": {
+                            "angle1": 1.5, "angle2": 0, "direction_type": "J2000"
+                        }
+                    }],
+                    "tile_beam":
+                    { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" },
+                    "first_beamlet": 0
+                }]
+            }]
+        }
+    """
+)
diff --git a/tests/observation/test_multi_station_observation.py b/tests/observation/test_multi_station_observation.py
index 619918632ebe16676f6eaf77bad609d4b30d0653..bbd1945d5f8071a2b873cb287110648bc5b9c293 100644
--- a/tests/observation/test_multi_station_observation.py
+++ b/tests/observation/test_multi_station_observation.py
@@ -1,135 +1,238 @@
-# -*- coding: utf-8 -*-
-#
-# This file is part of the LOFAR2.0 lofar-station-client project.
-#
-# Distributed under the terms of the APACHE license.
-# See LICENSE.txt for more info.
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
 
+from typing import List, Callable
 from unittest import mock
 import concurrent.futures
-from json import loads
-from tests import base
 
-from lofar_station_client.observation.multi_station_observation import (
-    MultiStationObservation,
+import lofar_station_client
+from lofar_station_client.observation import multi_station_observation
+
+from tests import base
+from tests.observation.specs import (
+    SPEC_SINGLE_HOST_SINGLE_FIELD_DICT,
+    SPEC_TWO_HOST_TWO_FIELD_DICT,
 )
 
 
-SPEC_DICT = loads(
-    """
-        {
-          "observation_id": 12345,
-          "stop_time": "2106-02-07T00:00:00",
-          "antenna_set": "ALL",
-          "antenna_mask": [0,1,2,9],
-          "filter": "HBA_110_190",
-          "SAPs": [{
-                "subbands": [10, 20, 30],
-                "pointing": {
-                    "angle1": 1.5, "angle2": 0, "direction_type": "J2000"
-                }
-          }],
-          "tile_beam":
-            { "angle1": 1.5, "angle2": 0, "direction_type": "J2000" },
-          "first_beamlet": 0
-        }
-    """
-)
+class TestMultiStationObservation(base.TestCase):
+    def setUp(self):
+        proxy_patcher = mock.patch.object(
+            lofar_station_client.observation.multi_station_observation,
+            "StationObservationFuture",
+            autospec=True,
+        )
+        self.m_station_obs_future = proxy_patcher.start()
+        self.addCleanup(proxy_patcher.stop)
 
-HOST_LIST = ["host1", "host2", "host3"]
+    def configure_obs_future_mock(
+        self, host: str, is_running: any = True, connected: bool = True
+    ):
+        """Configure StationObservationFuture mock and return it"""
 
+        m_obs_future = mock.Mock()
 
-class FakeFuture:
-    def __init__(self, **kwargs):
-        return
+        future_start_stop = concurrent.futures.Future()
+        future_start_stop.set_result(True)
 
-    def done(self):
-        return True
+        future_running = concurrent.futures.Future()
+        future_running.set_result(is_running)
 
-    def result(self):
-        return True
+        m_obs_future.start.return_value = future_start_stop
+        m_obs_future.stop.return_value = future_start_stop
+        type(m_obs_future).is_running = mock.PropertyMock(return_value=future_running)
+        type(m_obs_future).connected = mock.PropertyMock(return_value=connected)
+        type(m_obs_future).get_control_poxy = mock.PropertyMock(
+            return_value="A_real_control_proxy"
+        )
 
+        type(m_obs_future).observations = mock.PropertyMock(
+            return_value="A_real_observation_proxy1"
+        )
 
-class TestMultiStationObservation(base.TestCase):
-    @mock.patch(
-        "lofar_station_client.observation.multi_station_observation.StationFutures",
-        autospec=True,
-    )
-    def setUp(self, M_station):
-        self.station_mocks = [mock.Mock(), mock.Mock(), mock.Mock()]
+        type(m_obs_future).host = mock.PropertyMock(return_value=host)
 
-        future = concurrent.futures.Future()
-        future.set_result(True)
-
-        for i in self.station_mocks:
-            i.start.return_value = future
-            i.stop.return_value = future
-            type(i).is_running = mock.PropertyMock(return_value=future)
-            type(i).connected = mock.PropertyMock(return_value=True)
-            type(i).get_control_poxy = mock.PropertyMock(
-                return_value="A_real_control_proxy"
-            )
+        return m_obs_future
 
-        type(self.station_mocks[0]).observations = mock.PropertyMock(
-            return_value="A_real_observation_proxy1"
+    @mock.patch.object(multi_station_observation, "StationObservationFuture")
+    def test_construct_called_correct_arguments(self, m_station_obs_future):
+        m_host = "host1"
+        multi_station_observation.MultiStationObservation(
+            specifications=SPEC_SINGLE_HOST_SINGLE_FIELD_DICT, hosts=[m_host]
+        )
+
+        m_station_obs_future.assert_called_once_with(
+            SPEC_SINGLE_HOST_SINGLE_FIELD_DICT["stations"][0], m_host
+        )
+
+    def test_create_station_object(self):
+        """Test construction of MultiStationObs creates appropriate child objects"""
+        m_host = "host1"
+
+        t_multi_observation = multi_station_observation.MultiStationObservation(
+            specifications=SPEC_SINGLE_HOST_SINGLE_FIELD_DICT, hosts=[m_host]
         )
-        type(self.station_mocks[1]).observations = mock.PropertyMock(
-            return_value="A_real_observation_proxy2"
+
+        self.assertEqual(1, len(t_multi_observation._stations))
+
+    def test_create_multiple_station_objects(self):
+        """"""
+        m_hosts = ["host1", "host2"]
+
+        t_multi_observation = multi_station_observation.MultiStationObservation(
+            specifications=SPEC_TWO_HOST_TWO_FIELD_DICT, hosts=m_hosts
         )
-        type(self.station_mocks[2]).observations = mock.PropertyMock(
-            return_value="A_real_observation_proxy3"
+
+        self.assertEqual(2, len(t_multi_observation._stations))
+
+    def test_create_station_object_error(self):
+        """"""
+        m_host = ["host1"]
+
+        self.assertRaises(
+            ValueError,
+            multi_station_observation.MultiStationObservation,
+            specifications=SPEC_TWO_HOST_TWO_FIELD_DICT,
+            hosts=m_host,
         )
 
-        type(self.station_mocks[0]).host = mock.PropertyMock(return_value="host1")
-        type(self.station_mocks[1]).host = mock.PropertyMock(return_value="host2")
-        type(self.station_mocks[2]).host = mock.PropertyMock(return_value="host3")
+    def test_start_observation(self):
+        m_host = "host1"
+        m_station_host = self.configure_obs_future_mock(m_host)
+        self.m_station_obs_future.side_effect = [m_station_host]
 
-        # apply the mock
-        M_station.side_effect = self.station_mocks
-        self.M_station = M_station
-        self.observation = MultiStationObservation(
-            specification=SPEC_DICT, hosts=HOST_LIST
+        t_multi_observation = multi_station_observation.MultiStationObservation(
+            specifications=SPEC_SINGLE_HOST_SINGLE_FIELD_DICT, hosts=[m_host]
         )
 
-    def test_start(self):
-        self.observation.start()
-        self.assertEqual(self.M_station.call_count, 3)
+        t_multi_observation.start()
 
-    def test_stop(self):
-        self.observation.stop()
-        self.assertEqual(self.M_station.call_count, 3)
+        m_station_host.start.assert_called_once()
 
-    def test_proxies(self):
-        expected = {
-            "host1": self.station_mocks[0],
-            "host2": self.station_mocks[1],
-            "host3": self.station_mocks[2],
-        }
-        results = self.observation.observations
-        self.assertEqual(expected, results)
+    def test_stop_observation(self):
+        m_host = "host1"
+        m_station_host = self.configure_obs_future_mock(m_host)
+        self.m_station_obs_future.side_effect = [m_station_host]
 
-    def test_is_running(self):
-        expected = {"host1": True, "host2": True, "host3": True}
-        results = self.observation.is_running
-        self.assertEqual(expected, results)
+        t_multi_observation = multi_station_observation.MultiStationObservation(
+            specifications=SPEC_SINGLE_HOST_SINGLE_FIELD_DICT, hosts=[m_host]
+        )
 
-    def test_is_connected(self):
-        expected = {"host1": True, "host2": True, "host3": True}
-        results = self.observation.is_connected
+        t_multi_observation.stop()
+
+        m_station_host.stop.assert_called_once()
+
+    def setup_test(
+        self,
+        specification: dict,
+        hosts: List[str],
+        test: Callable[
+            [
+                multi_station_observation.MultiStationObservation,
+                List[mock.Mock],
+                List[str],
+            ],
+            None,
+        ],
+    ):
+        """Dynamically configure a test scenario and execute to test() for conditions"""
+        m_station_hosts = []
+        for host in hosts:
+            m_station_hosts.append(self.configure_obs_future_mock(host))
+
+        self.m_station_obs_future.side_effect = m_station_hosts
+
+        t_multi_observation = multi_station_observation.MultiStationObservation(
+            specifications=specification, hosts=hosts
+        )
+
+        test(t_multi_observation, m_station_hosts, hosts)
+
+    def case_observation_proxies(self, t_multi_observation, m_station_hosts, hosts):
+        """Test case to use with :py:func:`~setup_test` to test observations property"""
+        expected = {}
+        for index, host in enumerate(hosts):
+            expected[host] = m_station_hosts[index]
+
+        results = t_multi_observation.observations
         self.assertEqual(expected, results)
 
-    def test_no_connection(self):
-        """
-        Tests the multiStationObservation classes ability to handle disconnected
-        stations by providing exceptions instead of values
-        """
+    def test_observation_proxy(self):
+        self.setup_test(
+            SPEC_SINGLE_HOST_SINGLE_FIELD_DICT, ["host1"], self.case_observation_proxies
+        )
+
+    def test_observation_proxies(self):
+        self.setup_test(
+            SPEC_TWO_HOST_TWO_FIELD_DICT,
+            ["host1", "m_host2"],
+            self.case_observation_proxies,
+        )
+
+    def case_is_running(self, t_multi_observation, m_station_hosts, hosts):
+        expected = {}
+        for index, host in enumerate(hosts):
+            expected[host] = True
+
+        self.assertEqual(expected, t_multi_observation.is_running)
+
+    def test_observation_is_running_true(self):
+        self.setup_test(
+            SPEC_SINGLE_HOST_SINGLE_FIELD_DICT, ["host1"], self.case_is_running
+        )
+
+    def test_observations_is_running_true(self):
+        self.setup_test(
+            SPEC_TWO_HOST_TWO_FIELD_DICT, ["host1", "host2"], self.case_is_running
+        )
+
+    def case_is_connected(self, t_multi_observation, m_station_hosts, hosts):
+        expected = {}
+        for index, host in enumerate(hosts):
+            expected[host] = True
+
+        self.assertEqual(expected, t_multi_observation.is_connected)
+        self.assertTrue(t_multi_observation.all_connected)
 
+    def test_observation_is_connected_true(self):
+        self.setup_test(
+            SPEC_SINGLE_HOST_SINGLE_FIELD_DICT, ["host1"], self.case_is_connected
+        )
+
+    def test_observations_is_connected_true(self):
+        self.setup_test(
+            SPEC_TWO_HOST_TWO_FIELD_DICT, ["host1", "host2"], self.case_is_connected
+        )
+
+    def case_no_connection(self, t_multi_observation, m_station_hosts, hosts):
         future = concurrent.futures.Future()
         future.set_exception(Exception("oh no"))
 
-        for i in self.station_mocks:
-            type(i).is_running = mock.PropertyMock(return_value=future)
+        for m_station in m_station_hosts:
+            m_station.is_running = mock.PropertyMock(return_value=future)
 
-        expected = {"host1": False, "host2": False, "host3": False}
-        results = self.observation.is_running
-        self.assertEqual(expected, results)
+        expected = {}
+        for index, host in enumerate(hosts):
+            expected[host] = False
+
+        self.assertEqual(expected, t_multi_observation.is_running)
+
+    def test_no_connection_is_running(self):
+        hosts = ["host1", "host2"]
+        m_station_hosts = []
+        for host in hosts:
+            m_station_hosts.append(
+                self.configure_obs_future_mock(host, is_running=Exception("oh no"))
+            )
+
+        self.m_station_obs_future.side_effect = m_station_hosts
+
+        t_multi_observation = multi_station_observation.MultiStationObservation(
+            specifications=SPEC_TWO_HOST_TWO_FIELD_DICT, hosts=hosts
+        )
+
+        expected = {}
+        for index, host in enumerate(hosts):
+            expected[host] = False
+
+        self.assertEqual(expected, t_multi_observation.is_running)
diff --git a/tests/observation/test_station_observation_future.py b/tests/observation/test_station_observation_future.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac80a2845a9a4057461dcea56e3178f35743ced5
--- /dev/null
+++ b/tests/observation/test_station_observation_future.py
@@ -0,0 +1,112 @@
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+from json import dumps
+from unittest import mock
+
+from tango import DevFailed
+
+from lofar_station_client.observation import station_observation_future
+from lofar_station_client.observation.constants import (
+    OBSERVATION_CONTROL_DEVICE_NAME,
+    OBSERVATION_FIELD_DEVICE_NAME,
+)
+
+from tests import base
+from tests.observation.specs import (
+    SPEC_SINGLE_HOST_SINGLE_FIELD_DICT,
+    SPEC_TWO_HOST_TWO_FIELD_DICT,
+)
+
+
+class TestStationObservation(base.TestCase):
+    def setUp(self):
+        proxy_patcher = mock.patch.object(
+            station_observation_future,
+            "DeviceProxy",
+            autospec=True,
+        )
+        self.m_device_proxy = proxy_patcher.start()
+        self.addCleanup(proxy_patcher.stop)
+
+    def create_test_base(self, specification: dict, host: str):
+        t_obs_future = station_observation_future.StationObservationFuture(
+            specification, host
+        )
+
+        num_fields = len(specification["antenna_fields"])
+
+        # Did create correct control device
+        self.assertEqual(
+            ((f"tango://{t_obs_future.host}/" f"{OBSERVATION_CONTROL_DEVICE_NAME}",),),
+            self.m_device_proxy.call_args_list[0],
+        )
+
+        self.assertEqual(num_fields, len(t_obs_future._antenna_fields))
+        # self.assertEqual(num_fields, len(t_obs_future._observation_field_proxies))
+
+    def test_create_single_field(self):
+        self.create_test_base(SPEC_SINGLE_HOST_SINGLE_FIELD_DICT["stations"][0], "henk")
+
+    def test_create_two_field(self):
+        self.create_test_base(SPEC_TWO_HOST_TWO_FIELD_DICT["stations"][0], "henk")
+
+    def proxies_test_base(self, specification: dict, host: str):
+        t_obs_future = station_observation_future.StationObservationFuture(
+            specification, host
+        )
+
+        num_fields = len(specification["antenna_fields"])
+        proxies = t_obs_future.observation_field_proxies
+
+        for index, antenna_field in enumerate(specification["antenna_fields"]):
+            observation_id = antenna_field["observation_id"]
+            antenna_field = antenna_field["antenna_field"]
+
+            # Did create correct observation field device
+            self.assertEqual(
+                (
+                    (
+                        f"tango://{t_obs_future.host}/{OBSERVATION_FIELD_DEVICE_NAME}/"
+                        f"{observation_id}-{antenna_field}",
+                    ),
+                ),
+                self.m_device_proxy.call_args_list[index + 1],
+            )
+
+        self.assertEqual(num_fields, len(proxies))
+
+    def test_proxies_single_field(self):
+        self.proxies_test_base(
+            SPEC_SINGLE_HOST_SINGLE_FIELD_DICT["stations"][0], "host"
+        )
+
+    def test_proxies_two_field(self):
+        self.proxies_test_base(SPEC_TWO_HOST_TWO_FIELD_DICT["stations"][0], "host")
+
+    def test_not_connected(self):
+        self.m_device_proxy.side_effect = [DevFailed()]
+
+        t_obs_future = station_observation_future.StationObservationFuture(
+            SPEC_SINGLE_HOST_SINGLE_FIELD_DICT["stations"][0], "host"
+        )
+        self.assertFalse(t_obs_future.connected)
+
+    def test_connected(self):
+        t_obs_future = station_observation_future.StationObservationFuture(
+            SPEC_SINGLE_HOST_SINGLE_FIELD_DICT["stations"][0], "host"
+        )
+
+        self.assertTrue(t_obs_future.connected)
+
+    def test_start(self):
+        m_add = mock.Mock()
+        self.m_device_proxy.return_value = m_add
+        t_obs_future = station_observation_future.StationObservationFuture(
+            SPEC_SINGLE_HOST_SINGLE_FIELD_DICT["stations"][0], "host"
+        )
+
+        t_obs_future.start()
+        m_add.add_observation.assert_called_with(
+            dumps(t_obs_future._specification), wait=False
+        )
diff --git a/tox.ini b/tox.ini
index a2b09c4ce45a0ae27cf2d4bf706d13fd04ce01c7..962e79fc2925721a20a65c69d2078c11a75da5e1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -17,8 +17,8 @@ deps =
 commands_pre =
     {envpython} --version
 commands =
-    {envpython} --version
-    stestr run {posargs}
+    {envpython} -m pytest --version
+    {envpython} -m pytest -v --log-level=DEBUG --forked tests/{posargs}
 
 # Use generative name and command prefixes to reuse the same virtualenv
 # for all linting jobs.
@@ -27,7 +27,7 @@ usedevelop = False
 envdir = {toxworkdir}/linting
 commands =
     pep8: {envpython} -m flake8 --version
-    pep8: {envpython} -m flake8
+    pep8: {envpython} -m flake8 lofar_station_client tests
     black: {envpython} -m black --version
     black: {envpython} -m black --check --diff .
     pylint: {envpython} -m pylint --version
@@ -40,29 +40,31 @@ commands =
 [testenv:debug]
 commands = {envpython} -m testtools.run {posargs}
 
-[testenv:coverage]
-; stestr does not natively support generating coverage reports use
-; `PYTHON=python -m coverage run....` to overcome this.
+[testenv:{cover,coverage}]
+envdir = {toxworkdir}/coverage
 setenv =
-    PYTHON={envpython} -m coverage run --source lofar_station_client --omit='*tests*' --parallel-mode
+    VIRTUAL_ENV={envdir}
 commands =
-  {envpython} -m coverage erase
-  {envpython} -m stestr run {posargs}
-  {envpython} -m coverage combine
-  {envpython} -m coverage html -d cover --omit='*tests*'
-  {envpython} -m coverage xml -o coverage.xml
-  {envpython} -m coverage report --omit='*tests*'
+    {envpython} -m pytest --version
+    {envpython} -m coverage --version
+    {envpython} -m coverage erase
+    {envpython} -m pytest -v --log-level=DEBUG --cov-report term --cov-report html --cov-append --cov-report xml:coverage.xml --cov=lofar_station_client --forked tests/{posargs}
 
 [testenv:build]
 usedevelop = False
 commands = {envpython} -m build
 
 [testenv:docs]
+allowlist_externals =
+    sh
 deps =
   -r{toxinidir}/requirements.txt
   -r{toxinidir}/docs/requirements.txt
 extras = docs
-commands = sphinx-build -b html -W docs/source docs/build/html
+commands =
+    sh docs/cleanup.sh
+    sphinx-build --version
+    sphinx-build -b html -W docs/source docs/build/html
 
 [testenv:integration]
 # Do no install the lofar station client package, force packaged version install
@@ -79,4 +81,4 @@ commands =
 
 [flake8]
 filename = *.py
-exclude=.tox,.egg-info
+exclude=.tox,.egg-info,dist,_version,build