Repository: metoppv/improver

Issue:
Enable site cube input to `ConstructReliabilityCalibrationTables`
First part of the work to add site & 'point_by_point' functionality to reliability calibration (as detailed elsewhere), similar to what's been done in EMOS: https://github.com/metoppv/improver/pull/1170. Work will be split into separate PRs to aid reviewing.

Enable `ConstructReliabilityCalibrationTables` to accept both grid and site cubes as input forecast and truths. Proposed format of reliability table for site cube:

```
  reliability_calibration_table / (1) (air_temperature: 2; table_row_index: 3; probability_bin: 5; spot_index: xx)
       Dimension coordinates:
            air_temperature                           x                -               -                -                             
            table_row_index                           -                x               -                -                             
            probability_bin                           -                -               x                -                             
            spot_index                                -                -               -                x                             
       Auxiliary coordinates:
            table_row_name                            -                x               -                -                             
            latitude                                  -                -               -                x
            longitude                                 -                -               -                x
            wmo_ids                                   -                -               -                x
       Scalar coordinates:
            forecast_reference_time: xx
            forecast_period: 68400 seconds
```

Acceptance criteria:
 * `ConstructReliabilityCalibrationTables` accepts site cubes as well as grid cubes
 * Current tests pass
 * Add tests for site functionality

ping @gavinevans @bayliffe since you seem to have worked the most on reliability calibration. Please let me know of any questions/concerns.

cc @anja-bom @tjtg 


Discussion:
lucyleeow: Note tests are failing due to docs, fixed in #1668 
tjtg: #1668 is merged and I've re-run the CI tasks to pick up the fixes from that PR.
codecov[bot]: # [Codecov](https://codecov.io/gh/metoppv/improver/pull/1667?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv) Report
> Merging [#1667](https://codecov.io/gh/metoppv/improver/pull/1667?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv) (44ec54c) into [master](https://codecov.io/gh/metoppv/improver/commit/4f9c0aa14b6cfad96270c6bb6265ddc88a9a93c8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv) (4f9c0aa) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 44ec54c differs from pull request most recent head c35dc5f. Consider uploading reports for the commit c35dc5f to get more accurate results

[![Impacted file tree graph](https://codecov.io/gh/metoppv/improver/pull/1667/graphs/tree.svg?width=650&height=150&src=pr&token=FAc0tsgYFZ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv)](https://codecov.io/gh/metoppv/improver/pull/1667?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv)

```diff
@@           Coverage Diff           @@
##           master    #1667   +/-   ##
=======================================
  Coverage   98.18%   98.19%           
=======================================
  Files         110      110           
  Lines       10110    10118    +8     
=======================================
+ Hits         9927     9935    +8     
  Misses        183      183           
```


| [Impacted Files](https://codecov.io/gh/metoppv/improver/pull/1667?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv) | Coverage Δ | |
|---|---|---|
| [improver/calibration/reliability\_calibration.py](https://codecov.io/gh/metoppv/improver/pull/1667/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv#diff-aW1wcm92ZXIvY2FsaWJyYXRpb24vcmVsaWFiaWxpdHlfY2FsaWJyYXRpb24ucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/metoppv/improver/pull/1667?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/metoppv/improver/pull/1667?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv). Last update [4f9c0aa...c35dc5f](https://codecov.io/gh/metoppv/improver/pull/1667?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=metoppv).

lucyleeow: Note there is a PR to this PR that converts the unittests to pytest: https://github.com/lucyleeow/improver/pull/1

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1b394ade..b1feb7b8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -62,6 +62,7 @@ below:
 * Caroline Sandford (Met Office, UK)
 * Victoria Smart (Met Office, UK)
 * Eleanor Smith (Met Office, UK)
+* Marcus Spelman (Met Office, UK)
 * Belinda Trotta (Bureau of Meteorology, Australia)
 * Tomasz Trzeciak (Met Office, UK)
 * Mark Worsfold (Met Office, UK)
diff --git a/doc/source/about.rst b/doc/source/about.rst
index b3986007..8813caf6 100644
--- a/doc/source/about.rst
+++ b/doc/source/about.rst
@@ -67,14 +67,15 @@ Here we give a simple example of using an IMPROVER CLI to threshold data, moving
 
 .. code:: console
 
-    bin/improver threshold input_file.nc output_file.nc 5 10 15 20 --threshold_units m/s
+    bin/improver threshold input.nc --threshold-values 5,10,15,20 --threshold-units m/s --output output.nc
 
-
-* input_file.nc is a netCDF file containing a forecast diagnostic data cube, e.g. wind speeds across an x-y grid at a given time.
+* threshold is the name of the CLI to be called. In cases where the name of the CLI has underscores these should be written as hyphens e.g. snow_fraction would be written as snow-fraction.
+* input.nc is a netCDF file containing a forecast diagnostic data cube, e.g. wind speeds across an x-y grid at a given time.
 * for each threshold specified (5, 10, 15, 20 m/s) a new x-y grid of data will be created. Each point in the grid will contain a 0 if the input wind speed at that point was below the threshold, or 1 if it was above the threshold.
-* output_file.nc will be a new netCDF file containing the resulting data cube with an additional leading dimension that corresponds to the given thresholds (5, 10, 15, 20 m/s).
+* the threshold units specifies the units of the provided thresholds (m/s) and these will be converted to match the units of the original cube before thresholding the input cube.
+* output.nc will be a new netCDF file containing the resulting data cube with an additional leading dimension that corresponds to the given thresholds (5, 10, 15, 20 m/s).
 
-This simple example covers one step in a processing chain. Additional information about using any CLI can be found on the command line using `\\-\\-help` (or `-h`), e.g.:
+This simple example covers one step in a processing chain. Additional information about using any CLI can be found on the command line using `\-\-help` (or `-h`), e.g.:
 
 .. code:: console
 
diff --git a/improver/calibration/reliability_calibration.py b/improver/calibration/reliability_calibration.py
index a2dd9f38..ecd3491d 100644
--- a/improver/calibration/reliability_calibration.py
+++ b/improver/calibration/reliability_calibration.py
@@ -39,6 +39,7 @@ import numpy as np
 import scipy
 from iris.coords import AuxCoord, DimCoord
 from iris.cube import Cube, CubeList
+from iris.exceptions import CoordinateNotFoundError
 from numpy import ndarray
 from numpy.ma.core import MaskedArray
 
@@ -282,11 +283,20 @@ class ConstructReliabilityCalibrationTables(BasePlugin):
         non_spatial_coords = ["forecast_period", diagnostic]
 
         # Construct a list of coordinates in the desired order
-        dim_coords = [forecast.coord(axis=dim).name() for dim in ["x", "y"]]
-        dim_coords_and_dims = _get_coords_and_dims(dim_coords)
         aux_coords_and_dims = _get_coords_and_dims(non_spatial_coords)
-        dim_coords_and_dims.append((reliability_index_coord, 0))
         aux_coords_and_dims.append((reliability_name_coord, 0))
+        spatial_coords = [forecast.coord(axis=dim).name() for dim in ["x", "y"]]
+        spatial_coords_and_dims = _get_coords_and_dims(spatial_coords)
+        try:
+            spot_index_coord = _get_coords_and_dims(["spot_index"])
+            wmo_id_coord = _get_coords_and_dims(["wmo_id"])
+        except CoordinateNotFoundError:
+            dim_coords_and_dims = spatial_coords_and_dims
+        else:
+            dim_coords_and_dims = spot_index_coord
+            aux_coords_and_dims.extend(spatial_coords_and_dims + wmo_id_coord)
+
+        dim_coords_and_dims.append((reliability_index_coord, 0))
         dim_coords_and_dims.append((probability_bins_coord, 1))
 
         reliability_cube = iris.cube.Cube(
@@ -305,12 +315,12 @@ class ConstructReliabilityCalibrationTables(BasePlugin):
         self, forecast: Union[MaskedArray, ndarray], truth: Union[MaskedArray, ndarray]
     ) -> MaskedArray:
         """
-        For an x-y slice at a single validity time and threshold, populate
+        For a spatial slice at a single validity time and threshold, populate
         a reliability table using the provided truth.
 
         Args:
             forecast:
-                An array containing data over an xy slice for a single validity
+                An array containing data over a spatial slice for a single validity
                 time and threshold.
             truth:
                 An array containing a thresholded gridded truth at an
@@ -320,8 +330,8 @@ class ConstructReliabilityCalibrationTables(BasePlugin):
             An array containing reliability table data for a single time
             and threshold. The leading dimension corresponds to the rows
             of a calibration table, the second dimension to the number of
-            probability bins, and the trailing dimensions are the spatial
-            dimensions of the forecast and truth cubes (which are
+            probability bins, and the trailing dimension(s) are the spatial
+            dimension(s) of the forecast and truth cubes (which are
             equivalent).
         """
         observation_counts = []
@@ -338,7 +348,6 @@ class ConstructReliabilityCalibrationTables(BasePlugin):
             observation_counts.append(observation_mask)
             forecast_probabilities.append(forecasts_probability_values)
             forecast_counts.append(forecast_mask)
-
         reliability_table = np.ma.stack(
             [
                 np.ma.stack(observation_counts),


Test patch:
diff --git a/improver_tests/calibration/reliability_calibration/conftest.py b/improver_tests/calibration/reliability_calibration/conftest.py
new file mode 100644
index 00000000..3da5abdc
--- /dev/null
+++ b/improver_tests/calibration/reliability_calibration/conftest.py
@@ -0,0 +1,331 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# (C) British Crown Copyright 2017-2021 Met Office.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+"""Fixtures for reliability calibration tests."""
+
+from collections import namedtuple
+from datetime import datetime
+
+import numpy as np
+import pytest
+from iris.coords import DimCoord
+from iris.cube import CubeList
+
+from improver.spotdata.build_spotdata_cube import build_spotdata_cube
+from improver.synthetic_data.set_up_test_cubes import (
+    construct_scalar_time_coords,
+    set_up_probability_cube,
+)
+from improver.utilities.cube_manipulation import MergeCubes
+
+_thresholds = [283, 288]
+_dummy_point_locations = np.arange(9).astype(np.float32)
+_dummy_string_ids = [f"{i}" for i in range(9)]
+_threshold_coord = DimCoord(
+    _thresholds,
+    standard_name="air_temperature",
+    var_name="threshold",
+    units="K",
+    attributes={"spp__relative_to_threshold": "above"},
+)
+
+
+@pytest.fixture
+def expected_table_shape_grid():
+    return 3, 5, 3, 3
+
+
+@pytest.fixture
+def expected_table_shape_spot():
+    return 3, 5, 9
+
+
+@pytest.fixture
+def expected_attributes():
+    return {
+        "title": "Reliability calibration data table",
+        "source": "IMPROVER",
+        "institution": "unknown",
+    }
+
+
+@pytest.fixture
+def forecast_grid():
+    forecast_data = np.arange(9, dtype=np.float32).reshape(3, 3) / 8.0
+    forecast_data_stack = np.stack([forecast_data, forecast_data])
+    forecast_1 = set_up_probability_cube(forecast_data_stack, _thresholds)
+    forecast_2 = set_up_probability_cube(
+        forecast_data_stack,
+        _thresholds,
+        time=datetime(2017, 11, 11, 4, 0),
+        frt=datetime(2017, 11, 11, 0, 0),
+    )
+    forecasts_grid = MergeCubes()([forecast_1, forecast_2])
+    return forecasts_grid
+
+
+@pytest.fixture
+def truth_grid():
+    truth_data = np.linspace(281, 285, 9, dtype=np.float32).reshape(3, 3)
+    # Threshold the truths, giving fields of zeroes and ones.
+    truth_data_a = (truth_data > _thresholds[0]).astype(int)
+    truth_data_b = (truth_data > _thresholds[1]).astype(int)
+    truth_data_stack = np.stack([truth_data_a, truth_data_b])
+    truth_1 = set_up_probability_cube(
+        truth_data_stack, _thresholds, frt=datetime(2017, 11, 10, 4, 0)
+    )
+    truth_2 = set_up_probability_cube(
+        truth_data_stack,
+        _thresholds,
+        time=datetime(2017, 11, 11, 4, 0),
+        frt=datetime(2017, 11, 11, 4, 0),
+    )
+    truths_grid = MergeCubes()([truth_1, truth_2])
+    return truths_grid
+
+
+@pytest.fixture
+def forecast_spot(forecast_grid):
+    forecast_data = forecast_grid.data[0, ...]
+    spot_probabilities = forecast_data.reshape((2, 9))
+    forecasts_spot_list = CubeList()
+    for day in range(5, 7):
+        time_coords = construct_scalar_time_coords(
+            datetime(2017, 11, day, 4, 0), None, datetime(2017, 11, day, 0, 0),
+        )
+        time_coords = [t[0] for t in time_coords]
+        forecasts_spot_list.append(
+            build_spotdata_cube(
+                spot_probabilities,
+                name="probability_of_air_temperature_above_threshold",
+                units="1",
+                altitude=_dummy_point_locations,
+                latitude=_dummy_point_locations,
+                longitude=_dummy_point_locations,
+                wmo_id=_dummy_string_ids,
+                additional_dims=[_threshold_coord],
+                scalar_coords=time_coords,
+            )
+        )
+    forecasts_spot = forecasts_spot_list.merge_cube()
+    return forecasts_spot
+
+
+@pytest.fixture
+def truth_spot(truth_grid):
+    truth_data_spot = truth_grid[0, ...].data.reshape((2, 9))
+    truths_spot_list = CubeList()
+    for day in range(5, 7):
+        time_coords = construct_scalar_time_coords(
+            datetime(2017, 11, day, 4, 0), None, datetime(2017, 11, day, 4, 0),
+        )
+        time_coords = [t[0] for t in time_coords]
+        truths_spot_list.append(
+            build_spotdata_cube(
+                truth_data_spot,
+                name="probability_of_air_temperature_above_threshold",
+                units="1",
+                altitude=_dummy_point_locations,
+                latitude=_dummy_point_locations,
+                longitude=_dummy_point_locations,
+                wmo_id=_dummy_string_ids,
+                additional_dims=[_threshold_coord],
+                scalar_coords=time_coords,
+            )
+        )
+    truths_spot = truths_spot_list.merge_cube()
+    return truths_spot
+
+
+@pytest.fixture
+def masked_truths(truth_grid):
+    truth_data_grid = truth_grid.data[0, ...]
+    masked_array = np.zeros(truth_data_grid.shape, dtype=bool)
+    masked_array[:, 0, :2] = True
+    masked_truth_data_1 = np.ma.array(truth_data_grid, mask=masked_array)
+    masked_array = np.zeros(truth_data_grid.shape, dtype=bool)
+    masked_array[:, :2, 0] = True
+    masked_truth_data_2 = np.ma.array(truth_data_grid, mask=masked_array)
+    masked_truth_1 = set_up_probability_cube(
+        masked_truth_data_1, _thresholds, frt=datetime(2017, 11, 10, 4, 0)
+    )
+    masked_truth_2 = set_up_probability_cube(
+        masked_truth_data_2,
+        _thresholds,
+        time=datetime(2017, 11, 11, 4, 0),
+        frt=datetime(2017, 11, 11, 4, 0),
+    )
+    merged = MergeCubes()([masked_truth_1, masked_truth_2])
+    return merged
+
+
+@pytest.fixture
+def expected_table():
+    # Note the structure of the expected_table is non-trivial to interpret
+    # due to the dimension ordering.
+    return np.array(
+        [
+            [
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
+            ],
+            [
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.125, 0.25], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.375, 0.5, 0.625], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.75, 0.875, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
+            ],
+            [
+                [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 1.0, 1.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
+            ],
+        ],
+        dtype=np.float32,
+    )
+
+
+@pytest.fixture
+def expected_table_for_mask():
+    return np.array(
+        [
+            [
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
+            ],
+            [
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.25], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.375, 0.5, 0.625], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.75, 0.875, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
+            ],
+            [
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 1.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
+            ],
+        ],
+        dtype=np.float32,
+    )
+
+
+RelTableInputs = namedtuple("RelTableInputs", ["forecast", "truth", "expected_shape"])
+
+
+@pytest.fixture(params=["grid", "spot"])
+def create_rel_table_inputs(
+    request,
+    forecast_grid,
+    forecast_spot,
+    truth_grid,
+    truth_spot,
+    expected_table_shape_grid,
+    expected_table_shape_spot,
+):
+    if request.param == "grid":
+        return RelTableInputs(
+            forecast=forecast_grid,
+            truth=truth_grid,
+            expected_shape=expected_table_shape_grid,
+        )
+    return RelTableInputs(
+        forecast=forecast_spot,
+        truth=truth_spot,
+        expected_shape=expected_table_shape_spot,
+    )
+
+
+@pytest.fixture
+def reliability_cube(forecast_grid, expected_table):
+    from improver.calibration.reliability_calibration import (
+        ConstructReliabilityCalibrationTables as CalPlugin,
+    )
+
+    rel_cube_format = CalPlugin()._create_reliability_table_cube(
+        forecast_grid, forecast_grid.coord(var_name="threshold")
+    )
+    rel_cube = rel_cube_format.copy(data=expected_table)
+    return rel_cube
+
+
+@pytest.fixture
+def different_frt(reliability_cube):
+    diff_frt = reliability_cube.copy()
+    new_frt = diff_frt.coord("forecast_reference_time")
+    new_frt.points = new_frt.points + 48 * 3600
+    new_frt.bounds = new_frt.bounds + 48 * 3600
+    return diff_frt
+
+
+@pytest.fixture
+def masked_reliability_cube(reliability_cube):
+    masked_array = np.zeros(reliability_cube.shape, dtype=bool)
+    masked_array[:, :, 0, :2] = True
+    reliability_cube.data = np.ma.array(reliability_cube.data, mask=masked_array)
+    return reliability_cube
+
+
+@pytest.fixture
+def masked_different_frt(different_frt):
+    masked_array = np.zeros(different_frt.shape, dtype=bool)
+    masked_array[:, :, :2, 0] = True
+    different_frt.data = np.ma.array(different_frt.data, mask=masked_array)
+    return different_frt
+
+
+@pytest.fixture
+def overlapping_frt(reliability_cube):
+    new_frt = reliability_cube.coord("forecast_reference_time")
+    new_frt.points = new_frt.points + 6 * 3600
+    new_frt.bounds = new_frt.bounds + 6 * 3600
+    return reliability_cube
+
+
+@pytest.fixture
+def lat_lon_collapse():
+    return np.array(
+        [
+            [0.0, 0.0, 1.0, 2.0, 1.0],
+            [0.0, 0.375, 1.5, 1.625, 1.0],
+            [1.0, 2.0, 3.0, 2.0, 1.0],
+        ]
+    )
diff --git a/improver_tests/calibration/reliability_calibration/test_AggregateReliabilityCalibrationTables.py b/improver_tests/calibration/reliability_calibration/test_AggregateReliabilityCalibrationTables.py
index fb008445..00a3f4ca 100644
--- a/improver_tests/calibration/reliability_calibration/test_AggregateReliabilityCalibrationTables.py
+++ b/improver_tests/calibration/reliability_calibration/test_AggregateReliabilityCalibrationTables.py
@@ -30,228 +30,161 @@
 # POSSIBILITY OF SUCH DAMAGE.
 """Unit tests for the AggregateReliabilityCalibrationTables plugin."""
 
-import unittest
-
 import numpy as np
+import pytest
 from numpy.testing import assert_array_equal
 
 from improver.calibration.reliability_calibration import (
     AggregateReliabilityCalibrationTables as Plugin,
 )
-from improver.calibration.reliability_calibration import (
-    ConstructReliabilityCalibrationTables as CalPlugin,
-)
-from improver_tests.calibration.reliability_calibration.test_ConstructReliabilityCalibrationTables import (  # noqa: E501
-    Test_Setup,
-)
-
-
-class Test_Aggregation(Test_Setup):
-
-    """Test class for the Test_AggregateReliabilityCalibrationTables tests,
-    setting up cubes to use as inputs."""
-
-    def setUp(self):
-        """Create reliability calibration tables for testing."""
-
-        super().setUp()
-        reliability_cube_format = CalPlugin()._create_reliability_table_cube(
-            self.forecasts, self.expected_threshold_coord
-        )
-        self.reliability_cube = reliability_cube_format.copy(data=self.expected_table)
-        self.different_frt = self.reliability_cube.copy()
-        new_frt = self.different_frt.coord("forecast_reference_time")
-        new_frt.points = new_frt.points + 48 * 3600
-        new_frt.bounds = new_frt.bounds + 48 * 3600
-
-        self.masked_reliability_cube = self.reliability_cube.copy()
-        masked_array = np.zeros(self.reliability_cube.shape, dtype=bool)
-        masked_array[:, :, 0, :2] = True
-        self.masked_reliability_cube.data = np.ma.array(
-            self.reliability_cube.data, mask=masked_array
-        )
-
-        self.masked_different_frt = self.different_frt.copy()
-        masked_array = np.zeros(self.different_frt.shape, dtype=bool)
-        masked_array[:, :, :2, 0] = True
-        self.masked_different_frt.data = np.ma.array(
-            self.different_frt.data, mask=masked_array
-        )
-
-        self.overlapping_frt = self.reliability_cube.copy()
-        new_frt = self.overlapping_frt.coord("forecast_reference_time")
-        new_frt.points = new_frt.points + 6 * 3600
-        new_frt.bounds = new_frt.bounds + 6 * 3600
 
-        self.lat_lon_collapse = np.array(
-            [
-                [0.0, 0.0, 1.0, 2.0, 1.0],
-                [0.0, 0.375, 1.5, 1.625, 1.0],
-                [1.0, 2.0, 3.0, 2.0, 1.0],
-            ]
-        )
 
-
-class Test__repr__(unittest.TestCase):
-
-    """Test the __repr__ method."""
-
-    def test_basic(self):
-        """Test repr is as expected."""
-        self.assertEqual(str(Plugin()), "<AggregateReliabilityCalibrationTables>")
-
-
-class Test__check_frt_coord(Test_Aggregation):
-
-    """Test the _check_frt_coord method."""
-
-    def test_valid_bounds(self):
-        """Test that no exception is raised if the input cubes have forecast
-        reference time bounds that do not overlap."""
-
-        plugin = Plugin()
-        plugin._check_frt_coord([self.reliability_cube, self.different_frt])
-
-    def test_matching_bounds(self):
-        """Test that no exception is raised in the input cubes if a cube
-        contains matching bounds."""
-        lower_bound = self.reliability_cube.coord("forecast_reference_time").bounds[0][
-            0
+def test_frt_coord_valid_bounds(reliability_cube, different_frt):
+    """Test that no exception is raised if the input cubes have forecast
+    reference time bounds that do not overlap."""
+    plugin = Plugin()
+    plugin._check_frt_coord([reliability_cube, different_frt])
+
+
+def test_frt_coord_matching_bounds(reliability_cube, different_frt):
+    """Test that no exception is raised in the input cubes if a cube
+    contains matching bounds."""
+    lower_bound = reliability_cube.coord("forecast_reference_time").bounds[0][0]
+    reliability_cube.coord("forecast_reference_time").bounds = [
+        [lower_bound, lower_bound]
+    ]
+    plugin = Plugin()
+    plugin._check_frt_coord([reliability_cube, different_frt])
+
+
+def test_frt_coord_invalid_bounds(reliability_cube, overlapping_frt):
+    """Test that an exception is raised if the input cubes have forecast
+    reference time bounds that overlap."""
+
+    plugin = Plugin()
+    msg = "Reliability calibration tables have overlapping"
+    with pytest.raises(ValueError, match=msg):
+        plugin._check_frt_coord([reliability_cube, overlapping_frt])
+
+
+def test_process_aggregating_multiple_cubes(
+    reliability_cube, different_frt, expected_table
+):
+    """Test of aggregating two cubes without any additional coordinate
+    collapsing."""
+    frt = "forecast_reference_time"
+    expected_points = different_frt.coord(frt).points
+    expected_bounds = [
+        [
+            reliability_cube.coord(frt).bounds[0][0],
+            different_frt.coord(frt).bounds[-1][1],
         ]
-        self.reliability_cube.coord("forecast_reference_time").bounds = [
-            [lower_bound, lower_bound]
+    ]
+    plugin = Plugin()
+    result = plugin.process([reliability_cube, different_frt])
+    assert_array_equal(result.data, expected_table * 2)
+    assert_array_equal(result.shape, (3, 5, 3, 3))
+    assert_array_equal(result.coord(frt).points, expected_points)
+    assert_array_equal(result.coord(frt).bounds, expected_bounds)
+
+
+def test_process_aggregating_cubes_with_overlapping_frt(
+    reliability_cube, overlapping_frt
+):
+    """Test that attempting to aggregate reliability calibration tables
+    with overlapping forecast reference time bounds raises an exception.
+    The presence of overlapping forecast reference time bounds indicates
+    that the same forecast data has contributed to both tables, thus
+    aggregating them would double count these contributions."""
+
+    plugin = Plugin()
+    msg = "Reliability calibration tables have overlapping"
+    with pytest.raises(ValueError, match=msg):
+        plugin.process([reliability_cube, overlapping_frt])
+
+
+def test_process_aggregating_over_single_cube_coordinates(
+    reliability_cube, lat_lon_collapse
+):
+    """Test of aggregating over coordinates of a single cube. In this
+    instance the latitude and longitude coordinates are collapsed."""
+
+    frt = "forecast_reference_time"
+    expected_points = reliability_cube.coord(frt).points
+    expected_bounds = reliability_cube.coord(frt).bounds
+
+    plugin = Plugin()
+    result = plugin.process([reliability_cube], coordinates=["latitude", "longitude"])
+    assert_array_equal(result.data, lat_lon_collapse)
+    assert_array_equal(result.coord(frt).points, expected_points)
+    assert_array_equal(result.coord(frt).bounds, expected_bounds)
+
+
+def test_process_aggregating_over_cubes_and_coordinates(
+    reliability_cube, different_frt, lat_lon_collapse
+):
+    """Test of aggregating over coordinates and cubes in a single call. In
+    this instance the latitude and longitude coordinates are collapsed and
+    the values from two input cube combined."""
+
+    frt = "forecast_reference_time"
+    expected_points = different_frt.coord(frt).points
+    expected_bounds = [
+        [
+            reliability_cube.coord(frt).bounds[0][0],
+            different_frt.coord(frt).bounds[-1][1],
         ]
-        plugin = Plugin()
-        plugin._check_frt_coord([self.reliability_cube, self.different_frt])
-
-    def test_invalid_bounds(self):
-        """Test that an exception is raised if the input cubes have forecast
-        reference time bounds that overlap."""
-
-        plugin = Plugin()
-        msg = "Reliability calibration tables have overlapping"
-        with self.assertRaisesRegex(ValueError, msg):
-            plugin._check_frt_coord([self.reliability_cube, self.overlapping_frt])
-
-
-class Test_process(Test_Aggregation):
-
-    """Test the process method."""
-
-    def test_aggregating_multiple_cubes(self):
-        """Test of aggregating two cubes without any additional coordinate
-        collapsing."""
-
-        frt = "forecast_reference_time"
-        expected_points = self.different_frt.coord(frt).points
-        expected_bounds = [
-            [
-                self.reliability_cube.coord(frt).bounds[0][0],
-                self.different_frt.coord(frt).bounds[-1][1],
-            ]
+    ]
+
+    plugin = Plugin()
+    result = plugin.process(
+        [reliability_cube, different_frt], coordinates=["latitude", "longitude"],
+    )
+    assert_array_equal(result.data, lat_lon_collapse * 2)
+    assert_array_equal(result.coord(frt).points, expected_points)
+    assert_array_equal(result.coord(frt).bounds, expected_bounds)
+
+
+def test_process_aggregating_over_masked_cubes_and_coordinates(
+    masked_different_frt, masked_reliability_cube
+):
+    """Test of aggregating over coordinates and cubes in a single call
+    using a masked reliability table. In this instance the latitude and
+    longitude coordinates are collapsed and the values from two input cube
+    combined."""
+
+    frt = "forecast_reference_time"
+    expected_points = masked_different_frt.coord(frt).points
+    expected_bounds = [
+        [
+            masked_reliability_cube.coord(frt).bounds[0][0],
+            masked_different_frt.coord(frt).bounds[-1][1],
         ]
-
-        plugin = Plugin()
-        result = plugin.process([self.reliability_cube, self.different_frt])
-        assert_array_equal(result.data, self.expected_table * 2)
-        assert_array_equal(result.shape, (3, 5, 3, 3))
-        self.assertEqual(result.coord(frt).points, expected_points)
-        assert_array_equal(result.coord(frt).bounds, expected_bounds)
-
-    def test_aggregating_cubes_with_overlapping_frt(self):
-        """Test that attempting to aggregate reliability calibration tables
-        with overlapping forecast reference time bounds raises an exception.
-        The presence of overlapping forecast reference time bounds indicates
-        that the same forecast data has contributed to both tables, thus
-        aggregating them would double count these contributions."""
-
-        plugin = Plugin()
-        msg = "Reliability calibration tables have overlapping"
-        with self.assertRaisesRegex(ValueError, msg):
-            plugin.process([self.reliability_cube, self.overlapping_frt])
-
-    def test_aggregating_over_single_cube_coordinates(self):
-        """Test of aggregating over coordinates of a single cube. In this
-        instance the latitude and longitude coordinates are collapsed."""
-
-        frt = "forecast_reference_time"
-        expected_points = self.reliability_cube.coord(frt).points
-        expected_bounds = self.reliability_cube.coord(frt).bounds
-
-        plugin = Plugin()
-        result = plugin.process(
-            [self.reliability_cube], coordinates=["latitude", "longitude"]
-        )
-        assert_array_equal(result.data, self.lat_lon_collapse)
-        self.assertEqual(result.coord(frt).points, expected_points)
-        assert_array_equal(result.coord(frt).bounds, expected_bounds)
-
-    def test_aggregating_over_cubes_and_coordinates(self):
-        """Test of aggregating over coordinates and cubes in a single call. In
-        this instance the latitude and longitude coordinates are collapsed and
-        the values from two input cube combined."""
-
-        frt = "forecast_reference_time"
-        expected_points = self.different_frt.coord(frt).points
-        expected_bounds = [
-            [
-                self.reliability_cube.coord(frt).bounds[0][0],
-                self.different_frt.coord(frt).bounds[-1][1],
-            ]
+    ]
+    expected_result = np.array(
+        [
+            [0.0, 0.0, 2.0, 4.0, 2.0],
+            [0.0, 0.625, 2.625, 3.25, 2.0],
+            [0.0, 3.0, 5.0, 4.0, 2.0],
         ]
-
-        plugin = Plugin()
-        result = plugin.process(
-            [self.reliability_cube, self.different_frt],
-            coordinates=["latitude", "longitude"],
-        )
-        assert_array_equal(result.data, self.lat_lon_collapse * 2)
-        self.assertEqual(result.coord(frt).points, expected_points)
-        assert_array_equal(result.coord(frt).bounds, expected_bounds)
-
-    def test_aggregating_over_masked_cubes_and_coordinates(self):
-        """Test of aggregating over coordinates and cubes in a single call
-        using a masked reliability table. In this instance the latitude and
-        longitude coordinates are collapsed and the values from two input cube
-        combined."""
-
-        frt = "forecast_reference_time"
-        expected_points = self.masked_different_frt.coord(frt).points
-        expected_bounds = [
-            [
-                self.masked_reliability_cube.coord(frt).bounds[0][0],
-                self.masked_different_frt.coord(frt).bounds[-1][1],
-            ]
-        ]
-        expected_result = np.array(
-            [
-                [0.0, 0.0, 2.0, 4.0, 2.0],
-                [0.0, 0.625, 2.625, 3.25, 2.0],
-                [0.0, 3.0, 5.0, 4.0, 2.0],
-            ]
-        )
-
-        plugin = Plugin()
-        result = plugin.process(
-            [self.masked_reliability_cube, self.masked_different_frt],
-            coordinates=["latitude", "longitude"],
-        )
-        self.assertIsInstance(result.data, np.ma.MaskedArray)
-        assert_array_equal(result.data, expected_result)
-        self.assertEqual(result.coord(frt).points, expected_points)
-        assert_array_equal(result.coord(frt).bounds, expected_bounds)
-
-    def test_single_cube(self):
-        """Test the plugin returns an unaltered cube if only one is passed in
-        and no coordinates are given."""
-
-        plugin = Plugin()
-
-        expected = self.reliability_cube.copy()
-        result = plugin.process([self.reliability_cube])
-
-        self.assertEqual(result, expected)
-
-
-if __name__ == "__main__":
-    unittest.main()
+    )
+
+    plugin = Plugin()
+    result = plugin.process(
+        [masked_reliability_cube, masked_different_frt],
+        coordinates=["latitude", "longitude"],
+    )
+    assert isinstance(result.data, np.ma.MaskedArray)
+    assert_array_equal(result.data, expected_result)
+    assert_array_equal(result.coord(frt).points, expected_points)
+    assert_array_equal(result.coord(frt).bounds, expected_bounds)
+
+
+def test_single_cube(reliability_cube):
+    """Test the plugin returns an unaltered cube if only one is passed in
+    and no coordinates are given."""
+
+    plugin = Plugin()
+    expected = reliability_cube.copy()
+    result = plugin.process([reliability_cube])
+    assert result == expected
diff --git a/improver_tests/calibration/reliability_calibration/test_ConstructReliabilityCalibrationTables.py b/improver_tests/calibration/reliability_calibration/test_ConstructReliabilityCalibrationTables.py
index a42285b2..568a03a5 100644
--- a/improver_tests/calibration/reliability_calibration/test_ConstructReliabilityCalibrationTables.py
+++ b/improver_tests/calibration/reliability_calibration/test_ConstructReliabilityCalibrationTables.py
@@ -30,516 +30,348 @@
 # POSSIBILITY OF SUCH DAMAGE.
 """Unit tests for the ConstructReliabilityCalibrationTables plugin."""
 
-import unittest
-from datetime import datetime
-
 import iris
 import numpy as np
+import pytest
+from iris.coords import AuxCoord, DimCoord
+from iris.cube import Cube
 from numpy.testing import assert_allclose, assert_array_equal
 
 from improver.calibration.reliability_calibration import (
     ConstructReliabilityCalibrationTables as Plugin,
 )
-from improver.synthetic_data.set_up_test_cubes import set_up_probability_cube
-from improver.utilities.cube_manipulation import MergeCubes
-
-
-class Test_Setup(unittest.TestCase):
-
-    """Test class for the Test_ConstructReliabilityCalibrationTables tests,
-    setting up cubes to use as inputs."""
-
-    def setUp(self):
-        """Create forecast and truth cubes for use in testing the reliability
-        calibration plugin. Two forecast and two truth cubes are created, each
-        pair containing the same data but given different forecast reference
-        times and validity times. These times maintain the same forecast period
-        for each forecast cube.
-
-        The truth data for reliability calibration is thresholded data, giving
-        fields of zeroes and ones.
-
-        Each forecast cube in conjunction with the contemporaneous truth cube
-        will be used to produce a reliability calibration table. When testing
-        the process method here we expect the final reliability calibration
-        table for a given threshold (we are only using 283K in the value
-        comparisons) to be the sum of two of these identical tables."""
-
-        thresholds = [283, 288]
-        forecast_data = np.arange(9, dtype=np.float32).reshape(3, 3) / 8.0
-        forecast_data = np.stack([forecast_data, forecast_data])
-        truth_data = np.linspace(281, 285, 9, dtype=np.float32).reshape(3, 3)
-        # Threshold the truths, giving fields of zeroes and ones.
-        truth_data_a = (truth_data > thresholds[0]).astype(int)
-        truth_data_b = (truth_data > thresholds[1]).astype(int)
-        truth_data = np.stack([truth_data_a, truth_data_b])
-
-        self.forecast_1 = set_up_probability_cube(forecast_data, thresholds)
-        self.forecast_2 = set_up_probability_cube(
-            forecast_data,
-            thresholds,
-            time=datetime(2017, 11, 11, 4, 0),
-            frt=datetime(2017, 11, 11, 0, 0),
-        )
-        self.forecasts = MergeCubes()([self.forecast_1, self.forecast_2])
-        self.truth_1 = set_up_probability_cube(
-            truth_data, thresholds, frt=datetime(2017, 11, 10, 4, 0)
-        )
-        self.truth_2 = set_up_probability_cube(
-            truth_data,
-            thresholds,
-            time=datetime(2017, 11, 11, 4, 0),
-            frt=datetime(2017, 11, 11, 4, 0),
-        )
-        self.truths = MergeCubes()([self.truth_1, self.truth_2])
 
-        masked_array = np.zeros(truth_data.shape, dtype=bool)
-        masked_array[:, 0, :2] = True
-        masked_truth_data_1 = np.ma.array(truth_data, mask=masked_array)
-        masked_array = np.zeros(truth_data.shape, dtype=bool)
-        masked_array[:, :2, 0] = True
-        masked_truth_data_2 = np.ma.array(truth_data, mask=masked_array)
-
-        self.masked_truth_1 = set_up_probability_cube(
-            masked_truth_data_1, thresholds, frt=datetime(2017, 11, 10, 4, 0)
-        )
-        self.masked_truth_2 = set_up_probability_cube(
-            masked_truth_data_2,
-            thresholds,
-            time=datetime(2017, 11, 11, 4, 0),
-            frt=datetime(2017, 11, 11, 4, 0),
-        )
-        self.masked_truths = MergeCubes()([self.masked_truth_1, self.masked_truth_2])
-        self.expected_threshold_coord = self.forecasts.coord(var_name="threshold")
-        self.expected_table_shape = (3, 5, 3, 3)
-        self.expected_attributes = {
-            "title": "Reliability calibration data table",
-            "source": "IMPROVER",
-            "institution": "unknown",
-        }
-
-        # Note the structure of the expected_table is non-trivial to interpret
-        # due to the dimension ordering.
-        self.expected_table = np.array(
-            [
-                [
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
-                ],
-                [
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.125, 0.25], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.375, 0.5, 0.625], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.75, 0.875, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
-                ],
-                [
-                    [[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 1.0, 1.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
-                ],
-            ],
-            dtype=np.float32,
-        )
-        self.expected_table_for_mask = np.array(
-            [
-                [
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
-                ],
-                [
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.25], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.375, 0.5, 0.625], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.75, 0.875, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
-                ],
-                [
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 1.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
-                ],
-            ],
-            dtype=np.float32,
-        )
-
-
-class Test__init__(unittest.TestCase):
-
-    """Test the __init__ method."""
-
-    def test_using_defaults(self):
-        """Test without providing any arguments."""
-        plugin = Plugin()
-        self.assertEqual(len(plugin.probability_bins), 5)
-        self.assertEqual(plugin.expected_table_shape, (3, 5))
-
-    def test_with_arguments(self):
-        """Test with specified arguments."""
-        plugin = Plugin(
-            n_probability_bins=4,
-            single_value_lower_limit=False,
-            single_value_upper_limit=False,
-        )
-        self.assertEqual(len(plugin.probability_bins), 4)
-        self.assertEqual(plugin.expected_table_shape, (3, 4))
-
-
-class Test__repr__(unittest.TestCase):
-
-    """Test the __repr__ method."""
-
-    def test_basic(self):
-        """Test repr is as expected."""
-        plugin = Plugin(
+"""Create forecast and truth cubes for use in testing the reliability
+calibration plugin. Two forecast and two truth cubes are created, each
+pair containing the same data but given different forecast reference
+times and validity times. These times maintain the same forecast period
+for each forecast cube.
+
+The truth data for reliability calibration is thresholded data, giving
+fields of zeroes and ones.
+
+Each forecast cube in conjunction with the contemporaneous truth cube
+will be used to produce a reliability calibration table. When testing
+the process method here we expect the final reliability calibration
+table for a given threshold (we are only using 283K in the value
+comparisons) to be the sum of two of these identical tables."""
+
+
+def test_init_using_defaults():
+    """Test init method without providing any arguments."""
+    plugin = Plugin()
+    assert len(plugin.probability_bins) == 5
+    assert plugin.expected_table_shape == (3, 5)
+
+
+def test_init_with_arguments():
+    """Test init method with specified arguments."""
+    plugin = Plugin(
+        n_probability_bins=4,
+        single_value_lower_limit=False,
+        single_value_upper_limit=False,
+    )
+    assert len(plugin.probability_bins) == 4
+    assert plugin.expected_table_shape, (3, 4)
+
+
+def test_dpb_without_single_value_limits():
+    """Test the generation of probability bins without single value end
+    bins. The range 0 to 1 will be divided into 4 equally sized bins."""
+    expected = np.array(
+        [[0.0, 0.24999999], [0.25, 0.49999997], [0.5, 0.74999994], [0.75, 1.0]]
+    )
+    result = Plugin()._define_probability_bins(
+        n_probability_bins=4,
+        single_value_lower_limit=False,
+        single_value_upper_limit=False,
+    )
+    assert_allclose(result, expected)
+
+
+def test_dpb_with_both_single_value_limits():
+    """Test the generation of probability bins with both upper and lower
+    single value end bins. The range 0 to 1 will be divided into 2 equally
+    sized bins, with 2 end bins holding values approximately equal to 0 and 1."""
+    expected = np.array(
+        [
+            [0.0000000e00, 1.0000000e-06],
+            [1.0000001e-06, 4.9999997e-01],
+            [5.0000000e-01, 9.9999893e-01],
+            [9.9999899e-01, 1.0000000e00],
+        ]
+    )
+    result = Plugin()._define_probability_bins(
+        n_probability_bins=4,
+        single_value_lower_limit=True,
+        single_value_upper_limit=True,
+    )
+    assert_allclose(result, expected)
+
+
+def test_dpb_with_lower_single_value_limit():
+    """Test the generation of probability bins with only the lower single value
+    limit bin. The range 0 to 1 will be divided into 4 equally sized bins,
+    with 1 lower bin holding values approximately equal to 0."""
+    expected = np.array(
+        [
+            [0.0000000e00, 1.0000000e-06],
+            [1.0000001e-06, 3.3333331e-01],
+            [3.3333334e-01, 6.6666663e-01],
+            [6.6666669e-01, 1.0000000e00],
+        ],
+        dtype=np.float32,
+    )
+
+    result = Plugin()._define_probability_bins(
+        n_probability_bins=4,
+        single_value_lower_limit=True,
+        single_value_upper_limit=False,
+    )
+    assert_allclose(result, expected)
+
+
+def test_dpb_with_upper_single_value_limit():
+    """Test the generation of probability bins with only the upper single value
+    limit bin. The range 0 to 1 will be divided into 4 equally sized bins,
+    with 1 upper bin holding values approximately equal to 1."""
+    expected = np.array(
+        [
+            [0.0, 0.3333333],
+            [0.33333334, 0.6666666],
+            [0.6666667, 0.9999989],
+            [0.999999, 1.0],
+        ],
+        dtype=np.float32,
+    )
+
+    result = Plugin()._define_probability_bins(
+        n_probability_bins=4,
+        single_value_lower_limit=False,
+        single_value_upper_limit=True,
+    )
+    assert_allclose(result, expected)
+
+
+def test_dpb_with_both_single_value_limits_too_few_bins():
+    """In this test both lower and uppper single_value_limits are requested
+    whilst also trying to use 2 bins. This would leave no bins to cover the
+    range 0 to 1, so an error is raised."""
+
+    msg = (
+        "Cannot use both single_value_lower_limit and "
+        "single_value_upper_limit with 2 or fewer probability bins."
+    )
+    with pytest.raises(ValueError, match=msg):
+        Plugin()._define_probability_bins(
             n_probability_bins=2,
-            single_value_lower_limit=False,
-            single_value_upper_limit=False,
-        )
-        self.assertEqual(
-            str(plugin),
-            "<ConstructReliabilityCalibrationTables: probability_bins: "
-            "[0.00 --> 0.50], [0.50 --> 1.00]>",
-        )
-
-
-class Test__define_probability_bins(unittest.TestCase):
-
-    """Test the _define_probability_bins method."""
-
-    @staticmethod
-    def test_without_single_value_limits():
-        """Test the generation of probability bins without single value end
-        bins. The range 0 to 1 will be divided into 4 equally sized bins."""
-        expected = np.array(
-            [[0.0, 0.24999999], [0.25, 0.49999997], [0.5, 0.74999994], [0.75, 1.0]]
-        )
-        result = Plugin()._define_probability_bins(
-            n_probability_bins=4,
-            single_value_lower_limit=False,
-            single_value_upper_limit=False,
-        )
-        assert_allclose(result, expected)
-
-    @staticmethod
-    def test_with_both_single_value_limits():
-        """Test the generation of probability bins with both upper and lower
-        single value end bins. The range 0 to 1 will be divided into 2 equally
-        sized bins, with 2 end bins holding values approximately equal to 0 and 1."""
-        expected = np.array(
-            [
-                [0.0000000e00, 1.0000000e-06],
-                [1.0000001e-06, 4.9999997e-01],
-                [5.0000000e-01, 9.9999893e-01],
-                [9.9999899e-01, 1.0000000e00],
-            ]
-        )
-        result = Plugin()._define_probability_bins(
-            n_probability_bins=4,
             single_value_lower_limit=True,
             single_value_upper_limit=True,
         )
-        assert_allclose(result, expected)
-
-    @staticmethod
-    def test_with_lower_single_value_limit():
-        """Test the generation of probability bins with only the lower single value
-        limit bin. The range 0 to 1 will be divided into 4 equally sized bins,
-        with 1 lower bin holding values approximately equal to 0."""
-        expected = np.array(
-            [
-                [0.0000000e00, 1.0000000e-06],
-                [1.0000001e-06, 3.3333331e-01],
-                [3.3333334e-01, 6.6666663e-01],
-                [6.6666669e-01, 1.0000000e00],
-            ],
-            dtype=np.float32,
-        )
 
-        result = Plugin()._define_probability_bins(
-            n_probability_bins=4,
-            single_value_lower_limit=True,
-            single_value_upper_limit=False,
-        )
-        assert_allclose(result, expected)
-
-    @staticmethod
-    def test_with_upper_single_value_limit():
-        """Test the generation of probability bins with only the upper single value
-        limit bin. The range 0 to 1 will be divided into 4 equally sized bins,
-        with 1 upper bin holding values approximately equal to 1."""
-        expected = np.array(
+
+def test_cpb_coordinate_no_single_value_bins():
+    """Test the probability_bins coordinate has the expected values and
+    type with no single value lower and upper bins."""
+    expected_bounds = np.array([[0, 0.5], [0.5, 1]])
+    expected_points = np.mean(expected_bounds, axis=1)
+    plugin = Plugin(n_probability_bins=2,)
+    result = plugin._create_probability_bins_coord()
+
+    assert isinstance(result, iris.coords.DimCoord)
+    assert_allclose(result.points, expected_points)
+    assert_allclose(result.bounds, expected_bounds)
+
+
+def test_cpb_coordinate_single_value_bins():
+    """Test the probability_bins coordinate has the expected values and
+    type when using the single value lower and upper bins."""
+    expected_bounds = np.array(
+        [
+            [0.0000000e00, 1.0000000e-06],
+            [1.0000001e-06, 4.9999997e-01],
+            [5.0000000e-01, 9.9999893e-01],
+            [9.9999899e-01, 1.0000000e00],
+        ]
+    )
+    expected_points = np.mean(expected_bounds, axis=1)
+    plugin = Plugin(
+        n_probability_bins=4,
+        single_value_lower_limit=True,
+        single_value_upper_limit=True,
+    )
+    result = plugin._create_probability_bins_coord()
+
+    assert isinstance(result, DimCoord)
+    assert_allclose(result.points, expected_points)
+    assert_allclose(result.bounds, expected_bounds)
+
+
+def test_crt_coordinates():
+    """Test the reliability table coordinates have the expected values and
+    type."""
+    expected_indices = np.array([0, 1, 2], dtype=np.int32)
+    expected_names = np.array(
+        ["observation_count", "sum_of_forecast_probabilities", "forecast_count"]
+    )
+    index_coord, name_coord = Plugin()._create_reliability_table_coords()
+
+    assert isinstance(index_coord, DimCoord)
+    assert isinstance(name_coord, AuxCoord)
+    assert_array_equal(index_coord.points, expected_indices)
+    assert_array_equal(name_coord.points, expected_names)
+
+
+def test_metadata_with_complete_inputs(forecast_grid, expected_attributes):
+    """Test the metadata returned is complete and as expected when the
+    forecast cube contains the required metadata to copy."""
+    forecast_1 = forecast_grid[0]
+    forecast_1.attributes["institution"] = "Kitten Inc"
+    expected_attributes["institution"] = "Kitten Inc"
+    result = Plugin._define_metadata(forecast_1)
+    assert isinstance(result, dict)
+    assert result == expected_attributes
+
+
+def test_metadata_with_incomplete_inputs(forecast_grid, expected_attributes):
+    """Test the metadata returned is complete and as expected when the
+    forecast cube does not contain all the required metadata to copy."""
+    forecast_1 = forecast_grid[0]
+    result = Plugin._define_metadata(forecast_1)
+    assert isinstance(result, dict)
+    assert result == expected_attributes
+
+
+def test_valid_inputs(create_rel_table_inputs, expected_attributes):
+    """Tests correct reliability cube generated. Parameterized using
+    `create_rel_table_inputs` fixture."""
+    forecast_1 = create_rel_table_inputs.forecast[0]
+    forecast_slice = next(forecast_1.slices_over("air_temperature"))
+    result = Plugin()._create_reliability_table_cube(
+        forecast_slice, forecast_slice.coord(var_name="threshold")
+    )
+    assert isinstance(result, Cube)
+    assert result.shape == create_rel_table_inputs.expected_shape
+    assert result.name() == "reliability_calibration_table"
+    assert result.attributes == expected_attributes
+
+
+def test_prb_table_values(create_rel_table_inputs, expected_table):
+    """Test the reliability table returned has the expected values for the
+    given inputs. Parameterized using `create_rel_table_inputs` fixture."""
+    forecast_1 = create_rel_table_inputs.forecast[0]
+    truth_1 = create_rel_table_inputs.truth[0]
+    forecast_slice = next(forecast_1.slices_over("air_temperature"))
+    truth_slice = next(truth_1.slices_over("air_temperature"))
+    result = Plugin(
+        single_value_lower_limit=True, single_value_upper_limit=True
+    )._populate_reliability_bins(forecast_slice.data, truth_slice.data)
+
+    expected_table_shape = create_rel_table_inputs.expected_shape
+    assert result.shape == expected_table_shape
+    assert_array_equal(result, expected_table.reshape(expected_table_shape))
+
+
+def test_pmrb_table_values_masked_truth(
+    forecast_grid, masked_truths, expected_table_for_mask, expected_table_shape_grid
+):
+    """Test the reliability table returned has the expected values when a
+    masked truth is input."""
+    forecast_1 = forecast_grid[0]
+    masked_truth_1 = masked_truths[0]
+    forecast_slice = next(forecast_1.slices_over("air_temperature"))
+    truth_slice = next(masked_truth_1.slices_over("air_temperature"))
+    result = Plugin(
+        single_value_lower_limit=True, single_value_upper_limit=True
+    )._populate_masked_reliability_bins(forecast_slice.data, truth_slice.data)
+
+    assert result.shape == expected_table_shape_grid
+    assert np.ma.is_masked(result)
+    assert_array_equal(result.data, expected_table_for_mask)
+    expected_mask = np.zeros(expected_table_for_mask.shape, dtype=bool)
+    expected_mask[:, :, 0, :2] = True
+    assert_array_equal(result.mask, expected_mask)
+
+
+def test_process_return_type(forecast_grid, truth_grid):
+    """Test the process method returns a reliability table cube."""
+    result = Plugin().process(forecast_grid, truth_grid)
+    assert isinstance(result, iris.cube.Cube)
+    assert result.name() == "reliability_calibration_table"
+    assert result.coord("air_temperature") == forecast_grid.coord(var_name="threshold")
+    assert result.coord_dims("air_temperature")[0] == 0
+
+
+def test_process_table_values(create_rel_table_inputs, expected_table):
+    """Test that cube values are as expected, when process has
+    sliced the inputs up for processing and then summed the contributions
+    from the two dates. Note that the values tested here are for only one
+    of the two processed thresholds (283K). The results contain
+    contributions from two forecast/truth pairs.
+
+    Parameterized using `create_rel_table_inputs` fixture."""
+    expected = np.sum([expected_table, expected_table], axis=0)
+    result = Plugin(
+        single_value_lower_limit=True, single_value_upper_limit=True
+    ).process(create_rel_table_inputs.forecast, create_rel_table_inputs.truth)
+    assert_array_equal(
+        result[0].data, expected.reshape(create_rel_table_inputs.expected_shape)
+    )
+
+
+def test_table_values_masked_truth(
+    forecast_grid, masked_truths, expected_table_for_mask
+):
+    """Test, similar to test_table_values, using masked arrays. The
+    mask is different for different timesteps, reflecting the potential
+    for masked areas in e.g. a radar truth to differ between timesteps.
+    At timestep 1, two grid points are masked. At timestep 2, two
+    grid points are also masked with one masked grid point in common
+    between timesteps. As a result, only one grid point is masked (
+    within the upper left corner) within the resulting reliability table."""
+    expected_table_for_second_mask = np.array(
+        [
             [
-                [0.0, 0.3333333],
-                [0.33333334, 0.6666666],
-                [0.6666667, 0.9999989],
-                [0.999999, 1.0],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
             ],
-            dtype=np.float32,
-        )
-
-        result = Plugin()._define_probability_bins(
-            n_probability_bins=4,
-            single_value_lower_limit=False,
-            single_value_upper_limit=True,
-        )
-        assert_allclose(result, expected)
-
-    def test_with_both_single_value_limits_too_few_bins(self):
-        """In this test both lower and uppper single_value_limits are requested
-        whilst also trying to use 2 bins. This would leave no bins to cover the
-        range 0 to 1, so an error is raised."""
-
-        msg = (
-            "Cannot use both single_value_lower_limit and "
-            "single_value_upper_limit with 2 or fewer probability bins."
-        )
-        with self.assertRaisesRegex(ValueError, msg):
-            Plugin()._define_probability_bins(
-                n_probability_bins=2,
-                single_value_lower_limit=True,
-                single_value_upper_limit=True,
-            )
-
-
-class Test__create_probability_bins_coord(unittest.TestCase):
-
-    """Test the _create_probability_bins_coord method."""
-
-    def test_coordinate_no_single_value_bins(self):
-        """Test the probability_bins coordinate has the expected values and
-        type with no single value lower and upper bins."""
-        expected_bounds = np.array([[0, 0.5], [0.5, 1]])
-        expected_points = np.mean(expected_bounds, axis=1)
-        plugin = Plugin(n_probability_bins=2,)
-        result = plugin._create_probability_bins_coord()
-
-        self.assertIsInstance(result, iris.coords.DimCoord)
-        assert_allclose(result.points, expected_points)
-        assert_allclose(result.bounds, expected_bounds)
-
-    def test_coordinate_single_value_bins(self):
-        """Test the probability_bins coordinate has the expected values and
-        type when using the single value lower and upper bins."""
-        expected_bounds = np.array(
             [
-                [0.0000000e00, 1.0000000e-06],
-                [1.0000001e-06, 4.9999997e-01],
-                [5.0000000e-01, 9.9999893e-01],
-                [9.9999899e-01, 1.0000000e00],
-            ]
-        )
-        expected_points = np.mean(expected_bounds, axis=1)
-        plugin = Plugin(
-            n_probability_bins=4,
-            single_value_lower_limit=True,
-            single_value_upper_limit=True,
-        )
-        result = plugin._create_probability_bins_coord()
-
-        self.assertIsInstance(result, iris.coords.DimCoord)
-        assert_allclose(result.points, expected_points)
-        assert_allclose(result.bounds, expected_bounds)
-
-
-class Test__create_reliability_table_coords(unittest.TestCase):
-
-    """Test the _create_reliability_table_coords method."""
-
-    def test_coordinates(self):
-        """Test the reliability table coordinates have the expected values and
-        type."""
-        expected_indices = np.array([0, 1, 2], dtype=np.int32)
-        expected_names = np.array(
-            ["observation_count", "sum_of_forecast_probabilities", "forecast_count"]
-        )
-        index_coord, name_coord = Plugin()._create_reliability_table_coords()
-
-        self.assertIsInstance(index_coord, iris.coords.DimCoord)
-        self.assertIsInstance(name_coord, iris.coords.AuxCoord)
-        assert_array_equal(index_coord.points, expected_indices)
-        assert_array_equal(name_coord.points, expected_names)
-
-
-class Test__define_metadata(Test_Setup):
-
-    """Test the _define_metadata method."""
-
-    def test_metadata_with_complete_inputs(self):
-        """Test the metadata returned is complete and as expected when the
-        forecast cube contains the required metadata to copy."""
-
-        self.forecast_1.attributes["institution"] = "Kitten Inc"
-        self.expected_attributes["institution"] = "Kitten Inc"
-
-        result = Plugin._define_metadata(self.forecast_1)
-
-        self.assertIsInstance(result, dict)
-        self.assertEqual(result, self.expected_attributes)
-
-    def test_metadata_with_incomplete_inputs(self):
-        """Test the metadata returned is complete and as expected when the
-        forecast cube does not contain all the required metadata to copy."""
-
-        result = Plugin._define_metadata(self.forecast_1)
-
-        self.assertIsInstance(result, dict)
-        self.assertEqual(result, self.expected_attributes)
-
-
-class Test__create_reliability_table_cube(Test_Setup):
-
-    """Test the _create_reliability_table_cube method."""
-
-    def test_valid_inputs(self):
-        """Test the cube returned has the structure expected."""
-
-        forecast_slice = next(self.forecast_1.slices_over("air_temperature"))
-        result = Plugin()._create_reliability_table_cube(
-            forecast_slice, forecast_slice.coord(var_name="threshold")
-        )
-        self.assertIsInstance(result, iris.cube.Cube)
-        self.assertSequenceEqual(result.shape, self.expected_table_shape)
-        self.assertEqual(result.name(), "reliability_calibration_table")
-        self.assertEqual(result.attributes, self.expected_attributes)
-
-
-class Test__populate_reliability_bins(Test_Setup):
-
-    """Test the _populate_reliability_bins method."""
-
-    def test_table_values(self):
-        """Test the reliability table returned has the expected values for the
-        given inputs."""
-
-        forecast_slice = next(self.forecast_1.slices_over("air_temperature"))
-        truth_slice = next(self.truth_1.slices_over("air_temperature"))
-        result = Plugin(
-            single_value_lower_limit=True, single_value_upper_limit=True
-        )._populate_reliability_bins(forecast_slice.data, truth_slice.data)
-
-        self.assertSequenceEqual(result.shape, self.expected_table_shape)
-        assert_array_equal(result, self.expected_table)
-
-
-class Test__populate_masked_reliability_bins(Test_Setup):
-
-    """Test the _populate_masked_reliability_bins method."""
-
-    def test_table_values_masked_truth(self):
-        """Test the reliability table returned has the expected values when a
-        masked truth is input."""
-
-        forecast_slice = next(self.forecast_1.slices_over("air_temperature"))
-        truth_slice = next(self.masked_truth_1.slices_over("air_temperature"))
-        result = Plugin(
-            single_value_lower_limit=True, single_value_upper_limit=True
-        )._populate_masked_reliability_bins(forecast_slice.data, truth_slice.data)
-
-        self.assertSequenceEqual(result.shape, self.expected_table_shape)
-        self.assertTrue(np.ma.is_masked(result))
-        assert_array_equal(result.data, self.expected_table_for_mask)
-        expected_mask = np.zeros(self.expected_table_for_mask.shape, dtype=bool)
-        expected_mask[:, :, 0, :2] = True
-        assert_array_equal(result.mask, expected_mask)
-
-
-class Test_process(Test_Setup):
-
-    """Test the process method."""
-
-    def test_return_type(self):
-        """Test the process method returns a reliability table cube."""
-
-        result = Plugin().process(self.forecasts, self.truths)
-
-        self.assertIsInstance(result, iris.cube.Cube)
-        self.assertEqual(result.name(), "reliability_calibration_table")
-        self.assertEqual(result.coord("air_temperature"), self.expected_threshold_coord)
-        self.assertEqual(result.coord_dims("air_temperature")[0], 0)
-
-    def test_table_values(self):
-        """Test that cube values are as expected when process has sliced the
-        inputs up for processing and then summed the contributions from the
-        two dates. Note that the values tested here are for only one of the
-        two processed thresholds (283K). The results contain contributions
-        from two forecast/truth pairs."""
-
-        expected = np.sum([self.expected_table, self.expected_table], axis=0)
-        result = Plugin(
-            single_value_lower_limit=True, single_value_upper_limit=True
-        ).process(self.forecasts, self.truths)
-
-        assert_array_equal(result[0].data, expected)
-
-    def test_table_values_masked_truth(self):
-        """Test, similar to test_table_values, using masked arrays. The
-        mask is different for different timesteps, reflecting the potential
-        for masked areas in e.g. a radar truth to differ between timesteps.
-        At timestep 1, two grid points are masked. At timestep 2, two
-        grid points are also masked with one masked grid point in common
-        between timesteps. As a result, only one grid point is masked (
-        within the upper left corner) within the resulting reliability table."""
-
-        expected_table_for_second_mask = np.array(
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.125, 0.25], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.5, 0.625], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.75, 0.875, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
+            ],
             [
-                [
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
-                ],
-                [
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.125, 0.25], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.5, 0.625], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.75, 0.875, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
-                ],
-                [
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 1.0, 1.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 1.0, 1.0], [0.0, 0.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
-                    [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
-                ],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 1.0, 1.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 1.0, 1.0], [0.0, 0.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
+                [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
             ],
-            dtype=np.float32,
-        )
-        expected = np.sum(
-            [self.expected_table_for_mask, expected_table_for_second_mask], axis=0
-        )
-        expected_mask = np.zeros(expected.shape, dtype=bool)
-        expected_mask[:, :, 0, 0] = True
-        result = Plugin(
-            single_value_lower_limit=True, single_value_upper_limit=True
-        ).process(self.forecasts, self.masked_truths)
-        self.assertIsInstance(result.data, np.ma.MaskedArray)
-        assert_array_equal(result[0].data.data, expected)
-        assert_array_equal(result[0].data.mask, expected_mask)
-        # Different thresholds must have the same mask.
-        assert_array_equal(result[0].data.mask, result[1].data.mask)
-
-    def test_mismatching_threshold_coordinates(self):
-        """Test that an exception is raised if the forecast and truth cubes
-        have differing threshold coordinates."""
-
-        self.truths = self.truths[:, 0, ...]
-        msg = "Threshold coordinates differ between forecasts and truths."
-        with self.assertRaisesRegex(ValueError, msg):
-            Plugin().process(self.forecasts, self.truths)
-
-
-if __name__ == "__main__":
-    unittest.main()
+        ],
+        dtype=np.float32,
+    )
+    expected = np.sum([expected_table_for_mask, expected_table_for_second_mask], axis=0)
+    expected_mask = np.zeros(expected.shape, dtype=bool)
+    expected_mask[:, :, 0, 0] = True
+    result = Plugin(
+        single_value_lower_limit=True, single_value_upper_limit=True
+    ).process(forecast_grid, masked_truths)
+    assert isinstance(result.data, np.ma.MaskedArray)
+    assert_array_equal(result[0].data.data, expected)
+    assert_array_equal(result[0].data.mask, expected_mask)
+    # Different thresholds must have the same mask.
+    assert_array_equal(result[0].data.mask, result[1].data.mask)
+
+
+def test_process_mismatching_threshold_coordinates(truth_grid, forecast_grid):
+    """Test that an exception is raised if the forecast and truth cubes
+    have differing threshold coordinates."""
+    truths_grid = truth_grid[:, 0, ...]
+    msg = "Threshold coordinates differ between forecasts and truths."
+    with pytest.raises(ValueError, match=msg):
+        Plugin().process(forecast_grid, truths_grid)
diff --git a/improver_tests/calibration/utilities/conftest.py b/improver_tests/calibration/utilities/conftest.py
new file mode 100644
index 00000000..664036df
--- /dev/null
+++ b/improver_tests/calibration/utilities/conftest.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# (C) British Crown Copyright 2017-2021 Met Office.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+#   list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+#
+# * Neither the name of the copyright holder nor the names of its
+#   contributors may be used to endorse or promote products derived from
+#   this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+"""Fixtures for calibration utilities tests."""
+
+from ..reliability_calibration.conftest import (
+    different_frt,
+    expected_table,
+    forecast_grid,
+    reliability_cube,
+)
+
+_ = forecast_grid, reliability_cube, different_frt, expected_table
diff --git a/improver_tests/calibration/utilities/test_utilities.py b/improver_tests/calibration/utilities/test_utilities.py
index f66f9c79..75348b06 100644
--- a/improver_tests/calibration/utilities/test_utilities.py
+++ b/improver_tests/calibration/utilities/test_utilities.py
@@ -38,6 +38,7 @@ import unittest
 
 import iris
 import numpy as np
+from iris.coords import DimCoord
 from iris.cube import CubeList
 from iris.tests import IrisTest
 from iris.util import squeeze
@@ -63,9 +64,6 @@ from improver.synthetic_data.set_up_test_cubes import (
 )
 
 from ..ensemble_calibration.helper_functions import SetupCubes
-from ..reliability_calibration.test_AggregateReliabilityCalibrationTables import (
-    Test_Aggregation,
-)
 
 
 class Test_convert_cube_data_to_2d(IrisTest):
@@ -383,71 +381,69 @@ class Test__filter_non_matching_cubes(SetupCubes):
             filter_non_matching_cubes(self.partial_historic_forecasts, partial_truth)
 
 
-class Test_create_unified_frt_coord(Test_Aggregation):
-
-    """Test the create_unified_frt_coord method."""
-
-    def test_coordinate(self):
-        """Test the forecast reference time coordinate has the expected point,
-        bounds, and type for an input with multiple forecast reference time
-        points."""
-
-        frt = "forecast_reference_time"
-        frt_coord = self.forecasts.coord(frt)
-
-        expected_points = self.forecast_2.coord(frt).points[0]
-        expected_bounds = [[self.forecast_1.coord(frt).points[0], expected_points]]
-        result = create_unified_frt_coord(frt_coord)
-
-        self.assertIsInstance(result, iris.coords.DimCoord)
-        assert_array_equal(result.points, expected_points)
-        assert_array_equal(result.bounds, expected_bounds)
-        self.assertEqual(result.name(), frt_coord.name())
-        self.assertEqual(result.units, frt_coord.units)
-
-    def test_coordinate_single_frt_input(self):
-        """Test the forecast reference time coordinate has the expected point,
-        bounds, and type for an input with a single forecast reference time
-        point."""
-
-        frt = "forecast_reference_time"
-        frt_coord = self.forecast_1.coord(frt)
-
-        expected_points = self.forecast_1.coord(frt).points[0]
-        expected_bounds = [[self.forecast_1.coord(frt).points[0], expected_points]]
-        result = create_unified_frt_coord(frt_coord)
-
-        self.assertIsInstance(result, iris.coords.DimCoord)
-        assert_array_equal(result.points, expected_points)
-        assert_array_equal(result.bounds, expected_bounds)
-        self.assertEqual(result.name(), frt_coord.name())
-        self.assertEqual(result.units, frt_coord.units)
-
-    def test_coordinate_input_with_bounds(self):
-        """Test the forecast reference time coordinate has the expected point,
-        bounds, and type for an input multiple forecast reference times, each
-        with bounds."""
-
-        frt = "forecast_reference_time"
-        cube = iris.cube.CubeList(
-            [self.reliability_cube, self.different_frt]
-        ).merge_cube()
-        frt_coord = cube.coord(frt)
-
-        expected_points = self.different_frt.coord(frt).points[0]
-        expected_bounds = [
-            [
-                self.reliability_cube.coord(frt).bounds[0][0],
-                self.different_frt.coord(frt).bounds[0][-1],
-            ]
+def test_create_unified_frt_coordinate(forecast_grid):
+    """Test the forecast reference time coordinate has the expected point,
+    bounds, and type for an input with multiple forecast reference time
+    points."""
+
+    frt = "forecast_reference_time"
+    frt_coord = forecast_grid.coord(frt)
+    forecast_1 = forecast_grid[0, ...]
+    forecast_2 = forecast_grid[1, ...]
+    expected_points = forecast_2.coord(frt).points[0]
+    expected_bounds = [[forecast_1.coord(frt).points[0], expected_points]]
+    result = create_unified_frt_coord(frt_coord)
+
+    assert isinstance(result, DimCoord)
+    assert_array_equal(result.points, expected_points)
+    assert_array_equal(result.bounds, expected_bounds)
+    assert result.name() == frt_coord.name()
+    assert result.units == frt_coord.units
+
+
+def test_create_unified_frt_single_frt_input(forecast_grid):
+    """Test the forecast reference time coordinate has the expected point,
+    bounds, and type for an input with a single forecast reference time
+    point."""
+
+    frt = "forecast_reference_time"
+    forecast_1 = forecast_grid[0, ...]
+    frt_coord = forecast_1.coord(frt)
+
+    expected_points = forecast_1.coord(frt).points[0]
+    expected_bounds = [[forecast_1.coord(frt).points[0], expected_points]]
+    result = create_unified_frt_coord(frt_coord)
+
+    assert isinstance(result, iris.coords.DimCoord)
+    assert_array_equal(result.points, expected_points)
+    assert_array_equal(result.bounds, expected_bounds)
+    assert result.name() == frt_coord.name()
+    assert result.units == frt_coord.units
+
+
+def test_create_unified_frt_input_with_bounds(reliability_cube, different_frt):
+    """Test the forecast reference time coordinate has the expected point,
+    bounds, and type for an input multiple forecast reference times, each
+    with bounds."""
+
+    frt = "forecast_reference_time"
+    cube = iris.cube.CubeList([reliability_cube, different_frt]).merge_cube()
+    frt_coord = cube.coord(frt)
+
+    expected_points = different_frt.coord(frt).points[0]
+    expected_bounds = [
+        [
+            reliability_cube.coord(frt).bounds[0][0],
+            different_frt.coord(frt).bounds[0][-1],
         ]
-        result = create_unified_frt_coord(frt_coord)
-
-        self.assertIsInstance(result, iris.coords.DimCoord)
-        assert_array_equal(result.points, expected_points)
-        assert_array_equal(result.bounds, expected_bounds)
-        self.assertEqual(result.name(), frt_coord.name())
-        self.assertEqual(result.units, frt_coord.units)
+    ]
+    result = create_unified_frt_coord(frt_coord)
+
+    assert isinstance(result, iris.coords.DimCoord)
+    assert_array_equal(result.points, expected_points)
+    assert_array_equal(result.bounds, expected_bounds)
+    assert result.name() == frt_coord.name()
+    assert result.units == frt_coord.units
 
 
 class Test_merge_land_and_sea(IrisTest):