Repository: pace-neutrons/Euphonic (language: python)

Issue:
Test Optional Dependencies
Currently all (including optional) dependencies are installed before tests are run. Because of this we don't _really_ know if these dependencies are optional as we don't know if the tests will pass without them. Tests which depend on specific optional dependencies should be marked and run with them installed, but all other tests should be run without optional dependencies.


Discussion / hints:

Fix patch:
diff --git a/tox.ini b/tox.ini
index e21f741cd..bb328face 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,51 +1,91 @@
 [tox]
-# Use conda to set up the python environments to run in
-# Pin tox-conda 0.7.3 for now as tox-conda 0.8 requires conda 4.9.0
-# which isn't yet installed on all CI nodes
-requires = tox-conda==0.7.3
+requires = tox-conda
 # The python environments to run the tests in
-envlist = py36,py37,py38,py36-minrequirements-linux
+envlist = py37,py38,py36-{base,matplotlib,phonopy_reader,all},py36-minrequirements-linux
 # Skip the execution of setup.py as we do it with the correct arg in commands_pre below
 skipsdist = True
 
 [testenv]
 changedir = tests_and_analysis/test
+test_command = python run_tests.py --report
+
+[testenv:{py37,py38}]
 install_command =
     python -m pip install \
         --force-reinstall \
         --upgrade \
         --upgrade-strategy eager \
-        {opts} \
-        {packages}
+	{opts} \
+	{packages}
 deps =
     numpy
     -r{toxinidir}/tests_and_analysis/tox_requirements.txt
 commands_pre =
     python -m pip install \
-    --force-reinstall \
-    --upgrade \
-    --upgrade-strategy eager \
-    '{toxinidir}[matplotlib,phonopy_reader]'
-commands =
-    python run_tests.py --cov --report {posargs}
+        --upgrade \
+	--upgrade-strategy eager \
+	'{toxinidir}[matplotlib,phonopy_reader]'
+commands = {[testenv]test_command} --cov
+
+# Test with no extras
+[testenv:py36-base]
+install_command = {[testenv:py37]install_command}
+deps = {[testenv:py37]deps}
+commands_pre =
+    python -m pip install \
+        --upgrade \
+	--upgrade-strategy eager \
+        '{toxinidir}'
+commands = {[testenv]test_command} --cov -m "not (phonopy_reader or matplotlib)"
+
+# Test with matplotlib extra only
+[testenv:py36-matplotlib]
+install_command = {[testenv:py37]install_command}
+deps = {[testenv:py37]deps}
+commands_pre =
+    python -m pip install \
+        --upgrade \
+	--upgrade-strategy eager \
+        '{toxinidir}[matplotlib]'
+commands = {[testenv]test_command} --cov -m "matplotlib and not phonopy_reader"
+
+# Test with phonopy_reader extra only
+[testenv:py36-phonopy_reader]
+install_command = {[testenv:py37]install_command}
+deps = {[testenv:py37]deps}
+commands_pre =
+    python -m pip install \
+        --upgrade \
+	--upgrade-strategy eager \
+        '{toxinidir}[phonopy_reader]'
+commands = {[testenv]test_command} --cov -m "phonopy_reader and not matplotlib"
+
+# Run remaining tests that require multiple extras
+[testenv:py36-all]
+install_command = {[testenv:py37]install_command}
+deps = {[testenv:py37]deps}
+commands_pre =
+    python -m pip install \
+        --upgrade \
+	--upgrade-strategy eager \
+        '{toxinidir}[phonopy_reader,matplotlib]'
+commands = {[testenv]test_command} --cov -m "phonopy_reader and matplotlib"
 
 [testenv:py36-minrequirements-linux]
 whitelist_externals = rm
-changedir = tests_and_analysis/test
 install_command =
-    python -m pip install \
-    --force-reinstall \
-    {opts} {packages}
+    python -m pip install --force-reinstall {opts} {packages}
 platform =
     linux: linux
 deps =
     numpy==1.12.1
 commands_pre =
-    python -m pip install --force-reinstall -r{toxinidir}/tests_and_analysis/minimum_euphonic_requirements.txt
-    python -m pip install --force-reinstall -r{toxinidir}/tests_and_analysis/tox_requirements.txt
+    python -m pip install --force-reinstall \
+        -r{toxinidir}/tests_and_analysis/minimum_euphonic_requirements.txt
+    python -m pip install --force-reinstall \
+        -r{toxinidir}/tests_and_analysis/tox_requirements.txt
 # Force rebuild of euphonic extension to avoid Numpy clash
 # (it still exists from py36 env)
     rm -rf build
     python -m pip install '{toxinidir}[matplotlib,phonopy_reader]'
-commands =
-    python run_tests.py {posargs}
+commands = {[testenv]test_command}


Test patch:
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index 5a2a89b5b..773a5e657 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -37,11 +37,11 @@ jobs:
         if: startsWith(matrix.os, 'ubuntu')
         with:
           project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
-          coverage-reports: tests_and_analysis/test/reports/coverage.xml
+          coverage-reports: tests_and_analysis/test/reports/coverage*.xml
       - uses: codecov/codecov-action@v2
         if: startsWith(matrix.os, 'ubuntu')
         with:
-          files: tests_and_analysis/test/reports/coverage.xml
+          files: tests_and_analysis/test/reports/coverage*.xml
 
   publish-test-results:
     needs: test
diff --git a/tests_and_analysis/test/euphonic_test/test_calculate_structure_factor.py b/tests_and_analysis/test/euphonic_test/test_calculate_structure_factor.py
index 9c642a884..18232d83b 100644
--- a/tests_and_analysis/test/euphonic_test/test_calculate_structure_factor.py
+++ b/tests_and_analysis/test/euphonic_test/test_calculate_structure_factor.py
@@ -17,71 +17,47 @@
     check_structure_factor, get_sf_dir)
 
 
-@pytest.mark.integration
+def get_quartz_fc():
+    return ForceConstants.from_castep(
+        get_castep_path('quartz', 'quartz.castep_bin'))
+
+
+def get_si2_fc():
+    return ForceConstants.from_castep(
+        get_castep_path('Si2-sc-skew', 'Si2-sc-skew.castep_bin'))
+
+
 class TestCalculateStructureFactorFromForceConstants:
 
-    def get_multithreaded_kwargs(self, n_threads):
-        kwargs = {}
-        if n_threads == 0:
-            kwargs['use_c'] = False
-        else:
-            kwargs['use_c'] = True
-            kwargs['n_threads'] = n_threads
-        return kwargs
-
-    @pytest.fixture(params=[0, 1, 2])
-    def get_quartz_qpt_ph_modes_recip(self, request):
-        fc = ForceConstants.from_castep(
-            get_castep_path('quartz', 'quartz.castep_bin'))
-        kwargs = self.get_multithreaded_kwargs(request.param)
-        return fc.calculate_qpoint_phonon_modes(
-            get_test_qpts('split'), asr='reciprocal', **kwargs)
-
-    @pytest.fixture(params=[0, 1, 2])
-    def get_quartz_qpt_ph_modes(self, request):
-        fc = ForceConstants.from_castep(
-            get_castep_path('quartz', 'quartz.castep_bin'))
-        kwargs = self.get_multithreaded_kwargs(request.param)
-        return fc.calculate_qpoint_phonon_modes(
-            get_test_qpts('split'), **kwargs)
-
-    @pytest.fixture(params=[0, 1, 2])
-    def get_si2_qpt_ph_modes(self, request):
-        fc = ForceConstants.from_castep(
-            get_castep_path('Si2-sc-skew', 'Si2-sc-skew.castep_bin'))
-        kwargs = self.get_multithreaded_kwargs(request.param)
-        return fc.calculate_qpoint_phonon_modes(get_test_qpts(), **kwargs)
-
-    @pytest.fixture(params=[0, 1, 2])
-    def get_cahgo2_qpt_ph_modes(self, request):
-        fc = ForceConstants.from_phonopy(
-            path=get_phonopy_path('CaHgO2', ''),
-            summary_name='mp-7041-20180417.yaml')
-        kwargs = self.get_multithreaded_kwargs(request.param)
-        return fc.calculate_qpoint_phonon_modes(get_test_qpts(), **kwargs)
 
     @pytest.mark.parametrize(
-        'material, qpt_ph_modes, dw_file, expected_sf_file', [
-            ('quartz', pytest.lazy_fixture('get_quartz_qpt_ph_modes'),
+        'material, fc, qpt_ph_modes_args, dw_file, expected_sf_file', [
+            ('quartz', get_quartz_fc(), (get_test_qpts('split'), {}),
              'quartz_666_0K_debye_waller.json',
              'quartz_0K_fc_structure_factor.json'),
-            ('quartz', pytest.lazy_fixture('get_quartz_qpt_ph_modes'),
+            ('quartz', get_quartz_fc(), (get_test_qpts('split'), {}),
              'quartz_666_300K_debye_waller.json',
              'quartz_300K_fc_structure_factor.json'),
-            ('quartz', pytest.lazy_fixture('get_quartz_qpt_ph_modes'),
+            ('quartz', get_quartz_fc(), (get_test_qpts('split'), {}),
              None,
              'quartz_fc_structure_factor.json'),
-            ('Si2-sc-skew', pytest.lazy_fixture('get_si2_qpt_ph_modes'),
+            ('Si2-sc-skew', get_si2_fc(), (get_test_qpts(), {}),
              'Si2-sc-skew_666_300K_debye_waller.json',
              'Si2-sc-skew_300K_fc_structure_factor.json'),
-            ('CaHgO2', pytest.lazy_fixture('get_cahgo2_qpt_ph_modes'),
-             'CaHgO2_666_300K_debye_waller.json',
-             'CaHgO2_300K_fc_structure_factor.json'),
-            ('quartz', pytest.lazy_fixture('get_quartz_qpt_ph_modes_recip'),
+            ('quartz', get_quartz_fc(),
+             (get_test_qpts('split'), {'asr': 'reciprocal'}),
              'quartz_666_300K_debye_waller.json',
              'quartz_recip_asr_300K_fc_structure_factor.json')])
-    def test_calculate_structure_factor(self, material, qpt_ph_modes,
-                                        dw_file, expected_sf_file):
+    @pytest.mark.parametrize('n_threads', [0, 1, 2])
+    def test_calculate_structure_factor(self, material, fc, qpt_ph_modes_args,
+                                        dw_file, expected_sf_file, n_threads):
+        args, kwargs = qpt_ph_modes_args
+        if n_threads == 0:
+            kwargs['use_c'] = False
+        else:
+            kwargs['use_c'] = True
+            kwargs['n_threads'] = n_threads
+        qpt_ph_modes = fc.calculate_qpoint_phonon_modes(args, **kwargs)
         if dw_file is not None:
             sf = qpt_ph_modes.calculate_structure_factor(
                 dw=get_dw(material, dw_file))
@@ -89,37 +65,61 @@ def test_calculate_structure_factor(self, material, qpt_ph_modes,
             sf = qpt_ph_modes.calculate_structure_factor()
         sf_file = os.path.join(get_sf_dir(material), expected_sf_file)
         expected_sf = StructureFactor.from_json_file(sf_file)
-        tol_kwargs = {}
         # Use larger tolerances with reciprocal ASR - formalism works
         # only at gamma but is applied to all q, so problem is less
         # well conditioned leading to larger f.p errors on different systems
         if 'recip' in expected_sf_file:
-            tol_kwargs['sf_atol'] = 3e-3
-            tol_kwargs['sf_rtol'] = 3e-4
-            tol_kwargs['freq_atol'] = 1e-4
-            tol_kwargs['freq_rtol'] = 3e-4
-            tol_kwargs['freq_gamma_atol'] = 0.55
-            tol_kwargs['sf_gamma_atol'] = 3e2
-
+            tol_kwargs = {'sf_atol': 3e-3,
+                          'sf_rtol': 3e-4,
+                          'freq_atol': 1e-4,
+                          'freq_rtol': 3e-4,
+                          'freq_gamma_atol': 0.55,
+                          'sf_gamma_atol': 3e2}
+        else:
+            tol_kwargs = {}
         check_structure_factor(sf, expected_sf, **tol_kwargs)
 
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize(
+        'material, fc_kwargs, qpt_ph_modes_args, dw_file, expected_sf_file', [
+            ('CaHgO2', {'path': get_phonopy_path('CaHgO2', ''),
+                        'summary_name': 'mp-7041-20180417.yaml'},
+             (get_test_qpts(), {}),
+             'CaHgO2_666_300K_debye_waller.json',
+             'CaHgO2_300K_fc_structure_factor.json')])
+    @pytest.mark.parametrize('n_threads', [0, 1, 2])
+    def test_calculate_structure_factor_from_phonopy(
+            self, material, fc_kwargs, qpt_ph_modes_args, dw_file,
+            expected_sf_file, n_threads):
+        fc = ForceConstants.from_phonopy(**fc_kwargs)
+        args, kwargs = qpt_ph_modes_args
+        if n_threads == 0:
+            kwargs['use_c'] = False
+        else:
+            kwargs['use_c'] = True
+            kwargs['n_threads'] = n_threads
+        qpt_ph_modes = fc.calculate_qpoint_phonon_modes(args, **kwargs)
+        if dw_file is not None:
+            sf = qpt_ph_modes.calculate_structure_factor(
+                dw=get_dw(material, dw_file))
+        else:
+            sf = qpt_ph_modes.calculate_structure_factor()
+        sf_file = os.path.join(get_sf_dir(material), expected_sf_file)
+        expected_sf = StructureFactor.from_json_file(sf_file)
+        check_structure_factor(sf, expected_sf)
 
-@pytest.mark.unit
-class TestCalculateStructureFactorFromQpointPhononModes:
 
-    def get_quartz_qpt_ph_modes():
-        return QpointPhononModes.from_castep(
-            get_castep_path('quartz', 'quartz_nosplit.phonon'))
+def get_quartz_qpt_ph_modes():
+    return QpointPhononModes.from_castep(
+        get_castep_path('quartz', 'quartz_nosplit.phonon'))
+
 
-    def get_si2_qpt_ph_modes():
-        return QpointPhononModes.from_castep(
-            get_castep_path('Si2-sc-skew', 'Si2-sc-skew.phonon'))
+def get_si2_qpt_ph_modes():
+    return QpointPhononModes.from_castep(
+        get_castep_path('Si2-sc-skew', 'Si2-sc-skew.phonon'))
 
-    def get_cahgo2_qpt_ph_modes():
-        return QpointPhononModes.from_phonopy(
-            path=get_phonopy_path('CaHgO2', ''),
-            summary_name='mp-7041-20180417.yaml',
-            phonon_name='qpoints.yaml')
+
+class TestCalculateStructureFactorFromQpointPhononModes:
 
     @pytest.mark.parametrize(
         'material, qpt_ph_modes, dw_file, expected_sf_file', [
@@ -134,10 +134,7 @@ def get_cahgo2_qpt_ph_modes():
              'quartz_structure_factor.json'),
             ('Si2-sc-skew', get_si2_qpt_ph_modes(),
              'Si2-sc-skew_666_300K_debye_waller.json',
-             'Si2-sc-skew_300K_structure_factor.json'),
-            ('CaHgO2', get_cahgo2_qpt_ph_modes(),
-             'CaHgO2_666_300K_debye_waller.json',
-             'CaHgO2_300K_structure_factor.json')])
+             'Si2-sc-skew_300K_structure_factor.json')])
     def test_calculate_structure_factor(self, material, qpt_ph_modes,
                                         dw_file, expected_sf_file):
         if dw_file is not None:
@@ -149,6 +146,26 @@ def test_calculate_structure_factor(self, material, qpt_ph_modes,
         expected_sf = StructureFactor.from_json_file(sf_file)
         check_structure_factor(sf, expected_sf)
 
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize(
+        'material, qpt_ph_modes_kwargs, dw_file, expected_sf_file', [
+            ('CaHgO2', {'path': get_phonopy_path('CaHgO2', ''),
+             'summary_name': 'mp-7041-20180417.yaml',
+             'phonon_name': 'qpoints.yaml'},
+             'CaHgO2_666_300K_debye_waller.json',
+             'CaHgO2_300K_structure_factor.json')])
+    def test_calculate_structure_factor_from_phonopy(
+            self, material, qpt_ph_modes_kwargs, dw_file, expected_sf_file):
+        qpt_ph_modes = QpointPhononModes.from_phonopy(**qpt_ph_modes_kwargs)
+        if dw_file is not None:
+            sf = qpt_ph_modes.calculate_structure_factor(
+                dw=get_dw(material, dw_file))
+        else:
+            sf = qpt_ph_modes.calculate_structure_factor()
+        sf_file = os.path.join(get_sf_dir(material), expected_sf_file)
+        expected_sf = StructureFactor.from_json_file(sf_file)
+        check_structure_factor(sf, expected_sf)
+
     @pytest.mark.parametrize(
         'qpt_ph_modes, dw_material, dw_file', [
             (get_quartz_qpt_ph_modes(),
@@ -160,7 +177,6 @@ def test_incompatible_debye_waller_raises_valueerror(
                 dw=get_dw(dw_material, dw_file))
 
 
-@pytest.mark.unit
 class TestCalculateStructureFactorUsingReferenceData:
     @pytest.fixture
     def quartz_modes(self):
diff --git a/tests_and_analysis/test/euphonic_test/test_castep_reader.py b/tests_and_analysis/test/euphonic_test/test_castep_reader.py
index d77a30a7f..0dfd2a63b 100644
--- a/tests_and_analysis/test/euphonic_test/test_castep_reader.py
+++ b/tests_and_analysis/test/euphonic_test/test_castep_reader.py
@@ -15,7 +15,6 @@
 def get_filepath(filename):
     return os.path.join(get_data_path(), 'readers', filename)
 
-@pytest.mark.unit
 class TestReadPhononDosData:
 
     @pytest.mark.parametrize(
diff --git a/tests_and_analysis/test/euphonic_test/test_crystal.py b/tests_and_analysis/test/euphonic_test/test_crystal.py
index c3d431f69..6a278845c 100644
--- a/tests_and_analysis/test/euphonic_test/test_crystal.py
+++ b/tests_and_analysis/test/euphonic_test/test_crystal.py
@@ -115,7 +115,6 @@ def check_crystal(crystal, expected_crystal):
         atol=np.finfo(np.float64).eps)
 
 
-@pytest.mark.unit
 class TestCrystalCreation:
 
     @pytest.fixture(params=[get_expected_crystal('quartz'),
@@ -209,7 +208,6 @@ def test_faulty_creation(self, inject_faulty_elements):
             Crystal(*faulty_args)
 
 
-@pytest.mark.unit
 class TestCrystalSerialisation:
 
     @pytest.mark.parametrize('crystal', [
@@ -246,7 +244,6 @@ def test_serialise_to_dict(self, serialise_to_dict):
         check_crystal(crystal, expected_crystal)
 
 
-@pytest.mark.unit
 class TestCrystalUnitConversion:
 
     @pytest.mark.parametrize('material, attr, unit_val', [
@@ -267,7 +264,6 @@ def test_incorrect_unit_conversion(self, material, unit_attr,
             setattr(crystal, unit_attr, unit_val)
 
 
-@pytest.mark.unit
 class TestCrystalSetters:
 
     @pytest.mark.parametrize('material, attr, unit, scale', [
@@ -292,7 +288,6 @@ def test_incorrect_unit_conversion(self, material, attr,
             setattr(crystal, attr, new_attr)
 
 
-@pytest.mark.unit
 class TestCrystalMethods:
 
     quartz_reciprocal_cell = np.array([
diff --git a/tests_and_analysis/test/euphonic_test/test_debye_waller.py b/tests_and_analysis/test/euphonic_test/test_debye_waller.py
index b3e87aa37..7ed000a26 100644
--- a/tests_and_analysis/test/euphonic_test/test_debye_waller.py
+++ b/tests_and_analysis/test/euphonic_test/test_debye_waller.py
@@ -90,7 +90,6 @@ def check_debye_waller(
                         atol=dw_atol,
                         rtol=dw_rtol)
 
-@pytest.mark.unit
 class TestDebyeWallerCreation:
 
     @pytest.fixture(params=[
@@ -168,7 +167,6 @@ def test_faulty_creation(self, inject_faulty_elements):
             DebyeWaller(*faulty_args)
 
 
-@pytest.mark.unit
 class TestDebyeWallerSerialisation:
 
     @pytest.mark.parametrize('dw', [
@@ -201,7 +199,6 @@ def test_serialise_to_dict(self, serialise_to_dict):
         check_debye_waller(dw, expected_dw)
 
 
-@pytest.mark.unit
 class TestDebyeWallerUnitConversion:
 
     @pytest.mark.parametrize('material, json_file, attr, unit_val', [
@@ -226,7 +223,6 @@ def test_incorrect_unit_conversion(self, material, json_file, unit_attr,
             setattr(dw, unit_attr, unit_val)
 
 
-@pytest.mark.unit
 class TestDebyeWallerSetters:
 
     @pytest.mark.parametrize('material, json_file, attr, unit, scale', [
diff --git a/tests_and_analysis/test/euphonic_test/test_force_constants.py b/tests_and_analysis/test/euphonic_test/test_force_constants.py
index ae5717268..87d74e54f 100644
--- a/tests_and_analysis/test/euphonic_test/test_force_constants.py
+++ b/tests_and_analysis/test/euphonic_test/test_force_constants.py
@@ -161,46 +161,43 @@ def check_force_constants(
                 == expected_force_constants.dielectric.units)
 
 
-@pytest.mark.unit
 class TestForceConstantsCreation:
 
-    @pytest.fixture(params=[get_expected_fc('quartz'),
-                            get_expected_fc('LZO'),
-                            get_expected_fc('NaCl')])
-    def create_from_constructor(self, request):
-        expected_fc = request.param
+    @pytest.mark.parametrize('expected_fc', [
+        get_expected_fc('quartz'),
+        get_expected_fc('LZO'),
+        get_expected_fc('NaCl')])
+    def test_create_from_constructor(self, expected_fc):
         args, kwargs = expected_fc.to_constructor_args()
         fc = ForceConstants(*args, **kwargs)
-        return fc, expected_fc
+        check_force_constants(fc, expected_fc)
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('material, castep_bin_file', [
         ('LZO', 'La2Zr2O7.castep_bin'),
         ('graphite', 'graphite.castep_bin'),
         ('Si2-sc-skew', 'Si2-sc-skew.castep_bin'),
         ('quartz', 'quartz.castep_bin')])
-    def create_from_castep(self, request):
-        material, castep_bin_file = request.param
+    def test_create_from_castep(self, material, castep_bin_file):
         expected_fc = get_expected_fc(material)
         castep_filepath = get_castep_path(material, castep_bin_file)
         fc = ForceConstants.from_castep(castep_filepath)
-        return fc, expected_fc
+        check_force_constants(fc, expected_fc)
 
-    @pytest.fixture(params=['LZO', 'graphite', 'Si2-sc-skew', 'quartz',
-                            'CaHgO2', 'NaCl'])
-    def create_from_json(self, request):
-        material = request.param
+    @pytest.mark.parametrize('material',
+        ['LZO', 'graphite', 'Si2-sc-skew', 'quartz', 'CaHgO2', 'NaCl'])
+    def test_create_from_json(self, material):
         expected_fc = get_expected_fc(material)
         fc = ForceConstants.from_json_file(get_json_file(material))
-        return fc, expected_fc
+        check_force_constants(fc, expected_fc)
 
-    @pytest.fixture(params=['LZO', 'graphite', 'quartz'])
-    def create_from_dict(self, request):
-        material = request.param
+    @pytest.mark.parametrize('material', ['LZO', 'graphite', 'quartz'])
+    def test_create_from_dict(self, material):
         expected_fc = get_expected_fc(material)
         fc = ForceConstants.from_dict(expected_fc.to_dict())
-        return fc, expected_fc
+        check_force_constants(fc, expected_fc)
 
-    @pytest.fixture(params=[
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize('material, phonopy_args', [
         # Test all combinations of reading from .yaml with/without force
         # constants/born and different file formats. Extra files (e.g.
         # FORCE_CONSTANTS) have been renamed from their defaults to
@@ -242,24 +239,13 @@ def create_from_dict(self, request):
                     'fc_name': 'FULL_FORCE_CONSTANTS'}),
         ('CaHgO2', {'summary_name': 'phonopy_nofc.yaml',
                     'fc_name': 'full_force_constants.hdf5'})])
-    def create_from_phonopy(self, request):
-        material, phonopy_args = request.param
+    def test_create_from_phonopy(self, material, phonopy_args):
         phonopy_args['path'] = get_phonopy_path(material, '')
         fc = ForceConstants.from_phonopy(**phonopy_args)
         expected_fc = get_expected_fc(material)
-        return fc, expected_fc
-
-    @pytest.mark.parametrize(('force_constants_creator'), [
-        pytest.lazy_fixture('create_from_constructor'),
-        pytest.lazy_fixture('create_from_dict'),
-        pytest.lazy_fixture('create_from_json'),
-        pytest.lazy_fixture('create_from_phonopy'),
-        pytest.lazy_fixture('create_from_castep')])
-    def test_correct_object_creation(self, force_constants_creator):
-        force_constants, expected_force_constants = force_constants_creator
-        check_force_constants(force_constants, expected_force_constants)
+        check_force_constants(fc, expected_fc)
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('faulty_arg, faulty_value, expected_exception', [
         ('sc_matrix',
          get_expected_fc('quartz').sc_matrix[:2],
          ValueError),
@@ -293,15 +279,12 @@ def test_correct_object_creation(self, force_constants_creator):
         ('dielectric',
          get_expected_fc('quartz').dielectric.shape,
          TypeError)])
-    def inject_faulty_elements(self, request):
-        faulty_arg, faulty_value, expected_exception = request.param
+    def test_faulty_object_creation(
+            self, faulty_arg, faulty_value, expected_exception):
         expected_fc = get_expected_fc('quartz')
         # Inject the faulty value and get a tuple of constructor arguments
-        args, kwargs = expected_fc.to_constructor_args(**{faulty_arg: faulty_value})
-        return args, kwargs, expected_exception
-
-    def test_faulty_object_creation(self, inject_faulty_elements):
-        faulty_args, faulty_kwargs, expected_exception = inject_faulty_elements
+        faulty_args, faulty_kwargs = expected_fc.to_constructor_args(
+            **{faulty_arg: faulty_value})
         with pytest.raises(expected_exception):
             ForceConstants(*faulty_args, **faulty_kwargs)
 
@@ -322,6 +305,7 @@ def mocked_import(name, *args, **kwargs):
         with pytest.raises(ImportPhonopyReaderError):
             ForceConstants.from_phonopy(**phonopy_args)
 
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('phonopy_args, err', [
         ({'summary_name': 'phonopy_nofc.yaml',
           'fc_name': 'force_constants.hdf5',
@@ -342,6 +326,7 @@ def test_create_from_castep_with_no_fc_raises_runtime_error(self):
             ForceConstants.from_castep(
                 get_castep_path('h-BN', 'h-BN_no_force_constants.castep_bin'))
 
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('material, phonopy_args', [
         ('CaHgO2', {'summary_name': 'mp-7041-20180417.yaml'})])
     def test_create_from_phonopy_without_cloader_is_ok(
@@ -362,7 +347,6 @@ def mocked_import(name, globals, locals, fromlist, level):
         check_force_constants(fc, expected_fc)
 
 
-@pytest.mark.unit
 class TestForceConstantsSerialisation:
 
     @pytest.mark.parametrize('fc', [
@@ -392,7 +376,6 @@ def test_serialise_to_dict(self, serialise_to_dict):
         check_force_constants(fc, expected_fc)
 
 
-@pytest.mark.unit
 class TestForceConstantsUnitConversion:
 
     @pytest.mark.parametrize('material, attr, unit_val', [
@@ -414,7 +397,6 @@ def test_incorrect_unit_conversion(self, material, unit_attr,
             setattr(fc, unit_attr, unit_val)
 
 
-@pytest.mark.unit
 class TestForceConstantsSetters:
 
     @pytest.mark.parametrize('material, attr, unit, scale', [
diff --git a/tests_and_analysis/test/euphonic_test/test_force_constants_calculate_qpoint_frequencies.py b/tests_and_analysis/test/euphonic_test/test_force_constants_calculate_qpoint_frequencies.py
index 1c419f3a2..6d1c517f6 100644
--- a/tests_and_analysis/test/euphonic_test/test_force_constants_calculate_qpoint_frequencies.py
+++ b/tests_and_analysis/test/euphonic_test/test_force_constants_calculate_qpoint_frequencies.py
@@ -15,7 +15,6 @@
     get_fc, get_fc_dir)
 
 
-@pytest.mark.unit
 class TestForceConstantsCalculateQPointFrequencies:
 
     def get_lzo_fc():
diff --git a/tests_and_analysis/test/euphonic_test/test_force_constants_calculate_qpoint_phonon_modes.py b/tests_and_analysis/test/euphonic_test/test_force_constants_calculate_qpoint_phonon_modes.py
index b61067637..9dcf99190 100644
--- a/tests_and_analysis/test/euphonic_test/test_force_constants_calculate_qpoint_phonon_modes.py
+++ b/tests_and_analysis/test/euphonic_test/test_force_constants_calculate_qpoint_phonon_modes.py
@@ -18,7 +18,6 @@
     get_fc, get_fc_dir)
 
 
-@pytest.mark.unit
 class TestForceConstantsCalculateQPointPhononModes:
 
     def get_lzo_fc():
@@ -80,34 +79,10 @@ def get_quartz_fc():
          'quartz_split_reciprocal_qpoint_phonon_modes.json')]
 
 
-    nacl_params = [(
-        ForceConstants.from_phonopy(
-            path=get_phonopy_path('NaCl', ''),
-            summary_name='phonopy_nacl.yaml'),
-        'NaCl',
-        [get_test_qpts(), {'asr': 'reciprocal'}],
-        'NaCl_reciprocal_qpoint_phonon_modes.json')]
-
-
-    cahgo2_params = [(
-        ForceConstants.from_phonopy(
-            path=get_phonopy_path('CaHgO2', ''),
-            summary_name='mp-7041-20180417.yaml'),
-        'CaHgO2',
-        [get_test_qpts(), {'asr': 'reciprocal'}],
-        'CaHgO2_reciprocal_qpoint_phonon_modes.json')]
-
-
-    @pytest.mark.parametrize(
-        'fc, material, all_args, expected_qpoint_phonon_modes_file',
-        lzo_params + quartz_params + nacl_params + si2_params + cahgo2_params)
-    @pytest.mark.parametrize(
-        'reduce_qpts, n_threads',
-        [(False, 0), (True, 0), (True, 1), (True, 2)])
-    def test_calculate_qpoint_phonon_modes(
-            self, fc, material, all_args, expected_qpoint_phonon_modes_file,
-            reduce_qpts, n_threads):
-        func_kwargs = all_args[1]
+    @staticmethod
+    def calculate_and_check_qpoint_phonon_modes(
+            fc, material, args, expected_file, reduce_qpts, n_threads):
+        func_kwargs = args[1]
         func_kwargs['reduce_qpts'] = reduce_qpts
         if n_threads == 0:
             func_kwargs['use_c'] = False
@@ -115,10 +90,10 @@ def test_calculate_qpoint_phonon_modes(
             func_kwargs['use_c'] = True
             func_kwargs['n_threads'] = n_threads
         qpoint_phonon_modes = fc.calculate_qpoint_phonon_modes(
-            all_args[0], **func_kwargs)
+            args[0], **func_kwargs)
         expected_qpoint_phonon_modes = ExpectedQpointPhononModes(
             os.path.join(get_qpt_ph_modes_dir(material),
-                         expected_qpoint_phonon_modes_file))
+                         expected_file))
         # Only give gamma-acoustic modes special treatment if the acoustic
         # sum rule has been applied
         tol_kwargs = {}
@@ -133,6 +108,43 @@ def test_calculate_qpoint_phonon_modes(
                            expected_qpoint_phonon_modes,
                            **tol_kwargs)
 
+    @pytest.mark.parametrize(
+        'fc, material, all_args, expected_qpoint_phonon_modes_file',
+        lzo_params + quartz_params + si2_params)
+    @pytest.mark.parametrize(
+        'reduce_qpts, n_threads',
+        [(False, 0), (True, 0), (True, 1), (True, 2)])
+    def test_calculate_qpoint_phonon_modes(
+            self, fc, material, all_args, expected_qpoint_phonon_modes_file,
+            reduce_qpts, n_threads):
+        self.calculate_and_check_qpoint_phonon_modes(
+            fc, material, all_args, expected_qpoint_phonon_modes_file,
+            reduce_qpts, n_threads)
+
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize(
+        'fc_kwargs, material, all_args, expected_qpoint_phonon_modes_file', [
+        ({'path': get_phonopy_path('NaCl', ''),
+          'summary_name': 'phonopy_nacl.yaml'},
+         'NaCl',
+         [get_test_qpts(), {'asr': 'reciprocal'}],
+         'NaCl_reciprocal_qpoint_phonon_modes.json'),
+        ({'path': get_phonopy_path('CaHgO2', ''),
+          'summary_name': 'mp-7041-20180417.yaml'},
+         'CaHgO2',
+         [get_test_qpts(), {'asr': 'reciprocal'}],
+         'CaHgO2_reciprocal_qpoint_phonon_modes.json')])
+    @pytest.mark.parametrize(
+        'reduce_qpts, n_threads',
+        [(False, 0), (True, 0), (True, 1), (True, 2)])
+    def test_calculate_qpoint_phonon_modes_from_phonopy(
+            self, fc_kwargs, material, all_args,
+            expected_qpoint_phonon_modes_file, reduce_qpts, n_threads):
+        fc = ForceConstants.from_phonopy(**fc_kwargs)
+        self.calculate_and_check_qpoint_phonon_modes(
+            fc, material, all_args, expected_qpoint_phonon_modes_file,
+            reduce_qpts, n_threads)
+
     @pytest.mark.parametrize(
         ('fc, material, all_args, expected_qpoint_phonon_modes_file, '
          'expected_modg_file'), [
@@ -327,7 +339,6 @@ def test_calc_qpt_ph_mds_asr_with_nonsense_fc_raises_warning(
             fc.calculate_qpoint_phonon_modes(get_test_qpts(), asr=asr)
 
 
-@pytest.mark.unit
 class TestForceConstantsCalculateQPointPhononModesWithoutCExtensionInstalled:
 
     @pytest.fixture
@@ -363,7 +374,6 @@ def test_with_use_c_false_doesnt_raise_error_or_warn(
         assert len(warn_record) == 0
 
 
-@pytest.mark.unit
 class TestForceConstantsCalculateQPointPhononModesWithCExtensionInstalled:
 
     @pytest.fixture
diff --git a/tests_and_analysis/test/euphonic_test/test_install.py b/tests_and_analysis/test/euphonic_test/test_install.py
index a5da46dca..eb962f750 100644
--- a/tests_and_analysis/test/euphonic_test/test_install.py
+++ b/tests_and_analysis/test/euphonic_test/test_install.py
@@ -1,5 +1,6 @@
 from importlib_resources import open_text
-import yaml
+
+import pytest
 
 import euphonic
 
@@ -12,6 +13,11 @@ def test_license_is_installed(self):
         assert 'GNU GENERAL PUBLIC LICENSE' in license_data[0]
 
     def test_citation_cff_is_installed(self):
+        # yaml dependency is optional
+        try:
+            import yaml
+        except ModuleNotFoundError:
+            pytest.skip()
         with open_text(euphonic, 'CITATION.cff') as fp:
             citation_data = yaml.safe_load(fp)
         assert 'cff-version' in citation_data
diff --git a/tests_and_analysis/test/euphonic_test/test_plot.py b/tests_and_analysis/test/euphonic_test/test_plot.py
index 9a43a8f33..938c3d145 100644
--- a/tests_and_analysis/test/euphonic_test/test_plot.py
+++ b/tests_and_analysis/test/euphonic_test/test_plot.py
@@ -1,17 +1,24 @@
 import pytest
-import matplotlib
-matplotlib.use('Agg')
-import matplotlib.pyplot as plt
 import numpy as np
 import numpy.testing as npt
 
 from euphonic import ureg
-import euphonic.plot
-from euphonic.plot import plot_1d, plot_1d_to_axis
 from euphonic.spectra import Spectrum1D, Spectrum1DCollection
 
 from ..script_tests.utils import get_ax_image_data
 
+# Allow tests with matplotlib marker to be collected and
+# deselected if Matplotlib is not installed
+pytestmark = pytest.mark.matplotlib
+try:
+    import matplotlib
+    matplotlib.use('Agg')
+    import matplotlib.pyplot as plt
+
+    import euphonic.plot
+    from euphonic.plot import plot_1d, plot_1d_to_axis
+except ModuleNotFoundError:
+    pass
 
 @pytest.fixture
 def figure():
@@ -31,7 +38,6 @@ def axes_with_line_and_legend(axes):
     axes.legend()
     return axes
 
-@pytest.mark.unit
 def test_missing_matplotlib(mocker):
     from builtins import __import__ as builtins_import
     from importlib import reload
@@ -50,7 +56,6 @@ def mocked_import(name, *args, **kwargs):
             in mnf_error.value.args[0])
 
 
-@pytest.mark.unit
 class TestPlot1DCore:
     @pytest.mark.parametrize('spectra, expected_error',
                              [('wrong_type', TypeError), ])
@@ -176,7 +181,6 @@ def test_incorrect_length_labels_raises_value_error(
         with pytest.raises(ValueError):
             plot_1d_to_axis(spec, axes, labels=labels)
 
-@pytest.mark.unit
 class TestPlot1D:
     @staticmethod
     def mock_core(mocker):
@@ -291,7 +295,6 @@ def test_deprecated_kwargs_raise_deprecation_warning(
             plot_1d(spec1d, **kwargs)
 
 
-@pytest.mark.unit
 class TestPlot2D:
     @staticmethod
     def mock_core(mocker):
@@ -384,7 +387,6 @@ def test_deprecated_kwargs_raise_deprecation_warning(
             euphonic.plot.plot_2d(spectrum, **kwargs)
 
 
-@pytest.mark.unit
 @pytest.mark.parametrize('labels, rotate',
                          [([(1, 'A'), (3, 'B'), (4, 'CDEF')], False),
                           ([(0, 'A'), (3, 'THISISALONGLABEL')], True)])
diff --git a/tests_and_analysis/test/euphonic_test/test_powder.py b/tests_and_analysis/test/euphonic_test/test_powder.py
index 30333e49e..ef192e5ee 100644
--- a/tests_and_analysis/test/euphonic_test/test_powder.py
+++ b/tests_and_analysis/test/euphonic_test/test_powder.py
@@ -25,7 +25,6 @@ def jitter(request):
     return request.param
 
 
-@pytest.mark.unit
 @pytest.mark.parametrize('npts, sampling, sampling_args, sampling_kwargs',
                          [(10, 'golden', (10,), {'jitter': None}),
                           # 31 pts rounded up to 2N^2 -> 4 cols, 8 rows
@@ -143,7 +142,6 @@ def mock_fc(self, mocker, mock_qpm, mock_qpf, mock_crystal):
     _energy_bins = np.linspace(1., 10., 5)
     _scattering_lengths = {'Si': 4. * ureg('fm')}
 
-    @pytest.mark.unit
     @pytest.mark.parametrize('energy_bins', [_energy_bins, None])
     def test_sample_sphere_dos(self,
                                mocker, mock_fc, mock_qpf, random_qpts_array,
@@ -162,14 +160,12 @@ def test_sample_sphere_dos(self,
             mock_fc.calculate_qpoint_frequencies.call_args[0][0])
         mock_qpf.calculate_dos.assert_called_with(return_bins)
 
-    @pytest.mark.unit
     def test_sample_sphere_structure_factor_error(self, mock_fc, mock_dw):
         with pytest.raises(ValueError):
             sample_sphere_structure_factor(mock_fc, 1. * ureg('1 / angstrom'),
                                            dw=mock_dw,
                                            temperature=(100. * ureg('K')))
 
-    @pytest.mark.unit
     @pytest.mark.parametrize('options',
                              [dict(mod_q=1.2 * ureg('1 / angstrom'),
                                    npts=400, jitter=True,
@@ -276,7 +272,6 @@ def trivial_qpts(self):
                          [0, np.pi / 2, np.pi / 3]]}
 
     @staticmethod
-    @pytest.mark.unit
     def test_qpts_cart_to_frac_trivial(trivial_crystal, trivial_qpts):
         """Check internal method for q-point conversion with trivial example"""
         cart_qpts = np.asarray(trivial_qpts['cart'], dtype=float
@@ -287,7 +282,6 @@ def test_qpts_cart_to_frac_trivial(trivial_crystal, trivial_qpts):
         npt.assert_almost_equal(calc_frac_qpts, frac_qpts)
 
     @staticmethod
-    @pytest.mark.unit
     def test_qpts_cart_to_frac_roundtrip(random_qpts_array,
                                          nontrivial_crystal):
         frac_qpts = random_qpts_array
diff --git a/tests_and_analysis/test/euphonic_test/test_qpoint_frequencies.py b/tests_and_analysis/test/euphonic_test/test_qpoint_frequencies.py
index d3c05e489..af199a49b 100644
--- a/tests_and_analysis/test/euphonic_test/test_qpoint_frequencies.py
+++ b/tests_and_analysis/test/euphonic_test/test_qpoint_frequencies.py
@@ -125,47 +125,46 @@ def check_qpt_freqs(
         atol=np.finfo(np.float64).eps)
 
 
-@pytest.mark.unit
 class TestQpointFrequenciesCreation:
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('expected_qpt_freqs', [
         get_expected_qpt_freqs(
             'quartz', 'quartz_666_qpoint_frequencies.json'),
         get_expected_qpt_freqs(
             'quartz', 'quartz_bandstructure_qpoint_frequencies.json'),
         get_expected_qpt_freqs(
             'quartz', 'quartz_666_cv_only_qpoint_frequencies.json')])
-    def create_from_constructor(self, request):
-        expected_qpt_freqs = request.param
+    def test_create_from_constructor(self, expected_qpt_freqs):
         args, kwargs = expected_qpt_freqs.to_constructor_args()
         qpt_freqs = QpointFrequencies(*args, **kwargs)
-        return qpt_freqs, expected_qpt_freqs
+        check_qpt_freqs(qpt_freqs, expected_qpt_freqs)
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('expected_qpt_freqs', [
         get_expected_qpt_freqs(
             'quartz', 'quartz_bandstructure_qpoint_frequencies.json')])
-    def create_from_constructor_without_weights(self, request):
-        expected_qpt_freqs = request.param
+    def test_create_from_constructor_without_weights(
+            self, expected_qpt_freqs):
         args, kwargs = expected_qpt_freqs.to_constructor_args(weights=False)
         qpt_freqs = QpointFrequencies(*args, **kwargs)
-        return qpt_freqs, expected_qpt_freqs
+        check_qpt_freqs(qpt_freqs, expected_qpt_freqs)
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('material, phonon_file, json_file', [
         ('LZO', 'La2Zr2O7.phonon',
          'LZO_qpoint_frequencies.json'),
         ('quartz', 'quartz-666-grid.phonon',
          'quartz_666_qpoint_frequencies.json'),
         ('quartz', 'quartz_split_qpts.phonon',
          'quartz_split_from_castep_qpoint_frequencies.json')])
-    def create_from_castep(self, request):
-        material, phonon_file, json_file = request.param
+    def test_create_from_castep(self, material, phonon_file, json_file):
         qpt_freqs = QpointFrequencies.from_castep(
             get_castep_path(material, phonon_file))
         expected_qpt_freqs = ExpectedQpointFrequencies(
             os.path.join(get_qpt_freqs_dir(material), json_file))
-        return qpt_freqs, expected_qpt_freqs
+        check_qpt_freqs(qpt_freqs, expected_qpt_freqs)
 
-    @pytest.fixture(params=[
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize(
+            'material, subdir, phonopy_args, json_file', [
         ('NaCl', 'band', {'summary_name': 'should_not_be_read',
                           'phonon_name': 'band.yaml'},
          'NaCl_band_yaml_from_phonopy_qpoint_frequencies.json'),
@@ -195,48 +194,35 @@ def create_from_castep(self, request):
         ('CaHgO2', '', {'summary_name': 'mp-7041-20180417.yaml',
                         'phonon_name': 'qpoints.yaml'},
          'CaHgO2_from_phonopy_qpoint_frequencies.json')])
-    def create_from_phonopy(self, request):
-        material, subdir, phonopy_args, json_file = request.param
+    def test_create_from_phonopy(
+            self, material, subdir, phonopy_args, json_file):
         phonopy_args['path'] = get_phonopy_path(material, subdir)
         qpt_freqs = QpointFrequencies.from_phonopy(**phonopy_args)
         json_path = os.path.join(
             get_qpt_freqs_dir(material), json_file)
         expected_qpt_freqs = ExpectedQpointFrequencies(json_path)
-        return qpt_freqs, expected_qpt_freqs
+        check_qpt_freqs(qpt_freqs, expected_qpt_freqs)
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('material, json_file', [
         ('quartz', 'quartz_666_qpoint_frequencies.json'),
         ('quartz', 'quartz_bandstructure_qpoint_frequencies.json'),
         ('quartz', 'quartz_666_cv_only_qpoint_frequencies.json')])
-    def create_from_json(self, request):
-        material, json_file = request.param
+    def test_create_from_json(self, material, json_file):
         expected_qpt_freqs = get_expected_qpt_freqs(material, json_file)
         qpt_freqs = get_qpt_freqs(material, json_file)
-        return qpt_freqs, expected_qpt_freqs
+        check_qpt_freqs(qpt_freqs, expected_qpt_freqs)
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('material, json_file', [
         ('quartz', 'quartz_666_qpoint_frequencies.json'),
         ('quartz', 'quartz_bandstructure_qpoint_frequencies.json'),
         ('quartz', 'quartz_666_cv_only_qpoint_frequencies.json')])
-    def create_from_dict(self, request):
-        material, json_file = request.param
+    def test_create_from_dict(self, material, json_file):
         expected_qpt_freqs = get_expected_qpt_freqs(material, json_file)
         qpt_freqs = QpointFrequencies.from_dict(
             expected_qpt_freqs.to_dict())
-        return qpt_freqs, expected_qpt_freqs
-
-    @pytest.mark.parametrize(('qpt_freqs_creator'), [
-        pytest.lazy_fixture('create_from_constructor'),
-        pytest.lazy_fixture('create_from_constructor_without_weights'),
-        pytest.lazy_fixture('create_from_dict'),
-        pytest.lazy_fixture('create_from_json'),
-        pytest.lazy_fixture('create_from_castep'),
-        pytest.lazy_fixture('create_from_phonopy')])
-    def test_correct_object_creation(self, qpt_freqs_creator):
-        qpt_freqs, expected_qpt_freqs = qpt_freqs_creator
         check_qpt_freqs(qpt_freqs, expected_qpt_freqs)
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('faulty_arg, faulty_value, expected_exception', [
         ('qpts',
          get_expected_qpt_freqs(
              'quartz', 'quartz_666_qpoint_frequencies.json').qpts[:3],
@@ -259,19 +245,15 @@ def test_correct_object_creation(self, qpt_freqs_creator):
          get_expected_qpt_freqs(
              'quartz', 'quartz_666_qpoint_frequencies.json').weights[:5],
          ValueError)])
-    def inject_faulty_elements(self, request):
-        faulty_arg, faulty_value, expected_exception = request.param
+    def test_faulty_object_creation(
+            self, faulty_arg, faulty_value, expected_exception):
         expected_qpt_freqs = get_expected_qpt_freqs(
             'quartz', 'quartz_666_qpoint_frequencies.json')
         # Inject the faulty value and get a tuple of constructor arguments
         args, kwargs = expected_qpt_freqs.to_constructor_args(
             **{faulty_arg: faulty_value})
-        return args, kwargs, expected_exception
-
-    def test_faulty_object_creation(self, inject_faulty_elements):
-        faulty_args, faulty_kwargs, expected_exception = inject_faulty_elements
         with pytest.raises(expected_exception):
-            QpointFrequencies(*faulty_args, **faulty_kwargs)
+            QpointFrequencies(*args, **kwargs)
 
     @pytest.mark.parametrize('material, subdir, phonopy_args', [
         ('NaCl', 'qpoints', {'summary_name': 'phonopy.yaml',
@@ -292,6 +274,7 @@ def mocked_import(name, *args, **kwargs):
         with pytest.raises(ImportPhonopyReaderError):
             QpointFrequencies.from_phonopy(**phonopy_args)
 
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('material, subdir, phonopy_args, err', [
         ('NaCl', 'qpoints', {'summary_name': 'phonopy.yaml',
                              'phonon_name': 'qpoints_hdf5.test'},
@@ -310,6 +293,7 @@ def test_create_from_phonopy_with_bad_inputs_raises_err(
         with pytest.raises(err):
             QpointFrequencies.from_phonopy(**phonopy_args)
 
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('material, subdir, phonopy_args, json_file', [
         ('CaHgO2', '', {'summary_name': 'mp-7041-20180417.yaml',
                         'phonon_name': 'qpoints.yaml'},
@@ -334,7 +318,6 @@ def mocked_import(name, globals, locals, fromlist, level):
         check_qpt_freqs(qpt_freqs, expected_qpt_freqs)
 
 
-@pytest.mark.unit
 class TestQpointFrequenciesSerialisation:
 
     @pytest.mark.parametrize('qpt_freqs', [
@@ -364,7 +347,6 @@ def test_serialise_to_dict(self, serialise_to_dict):
         check_qpt_freqs(qpt_freqs, qpt_freqs_from_dict)
 
 
-@pytest.mark.unit
 class TestQpointFrequenciesUnitConversion:
 
     @pytest.mark.parametrize('material, json_file, attr, unit_val', [
@@ -388,7 +370,6 @@ def test_incorrect_unit_conversion(self, material, json_file, unit_attr,
             setattr(qpt_freqs, unit_attr, unit_val)
 
 
-@pytest.mark.unit
 class TestQpointFrequenciesSetters:
 
     @pytest.mark.parametrize('material, json_file, attr, unit, scale', [
@@ -413,7 +394,6 @@ def test_incorrect_unit_conversion(self, material, json_file, attr,
             setattr(qpt_freqs, attr, new_attr)
 
 
-@pytest.mark.unit
 class TestQpointFrequenciesCalculateDos:
 
     @pytest.mark.parametrize(
@@ -503,7 +483,6 @@ def test_calculate_dos_with_0_inv_cm_bin_doesnt_raise_runtime_warn(self):
             dos = qpt_freqs.calculate_dos(ebins)
         assert len(warn_record) == 0
 
-@pytest.mark.unit
 class TestQpointFrequenciesCalculateDosMap:
     @pytest.mark.parametrize(
         'material, qpt_freqs_json, ebins, expected_dos_map_json', [
@@ -560,7 +539,6 @@ def test_calculate_dos_map_gives_same_result_as_dos_at_single_qpt(
             dos_map_at_qpt = Spectrum1D(dos_map.y_data, dos_map.z_data[qpt])
             check_spectrum1d(dos_map_at_qpt, dos_single_qpt)
 
-@pytest.mark.unit
 class TestQpointFrequenciesGetDispersion:
 
     @pytest.mark.parametrize(
diff --git a/tests_and_analysis/test/euphonic_test/test_qpoint_phonon_modes.py b/tests_and_analysis/test/euphonic_test/test_qpoint_phonon_modes.py
index 069902f42..6bfeb45e9 100644
--- a/tests_and_analysis/test/euphonic_test/test_qpoint_phonon_modes.py
+++ b/tests_and_analysis/test/euphonic_test/test_qpoint_phonon_modes.py
@@ -157,27 +157,29 @@ def check_qpt_ph_modes(
         atol=np.finfo(np.float64).eps)
 
 
-@pytest.mark.unit
 class TestQpointPhononModesCreation:
 
-    @pytest.fixture(params=[get_expected_qpt_ph_modes('quartz'),
-                            get_expected_qpt_ph_modes('LZO'),
-                            get_expected_qpt_ph_modes('NaCl')])
-    def create_from_constructor(self, request):
-        expected_qpt_ph_modes = request.param
+    @pytest.mark.parametrize('expected_qpt_ph_modes', [
+        get_expected_qpt_ph_modes('quartz'),
+        get_expected_qpt_ph_modes('LZO'),
+        get_expected_qpt_ph_modes('NaCl')])
+    def test_create_from_constructor(self, expected_qpt_ph_modes):
         args, kwargs = expected_qpt_ph_modes.to_constructor_args()
         qpt_ph_modes = QpointPhononModes(*args, **kwargs)
-        return qpt_ph_modes, expected_qpt_ph_modes
+        check_qpt_ph_modes(qpt_ph_modes, expected_qpt_ph_modes,
+                           check_evecs=True)
 
-    @pytest.fixture(params=[get_expected_qpt_ph_modes('quartz'),
-                            get_expected_qpt_ph_modes('NaCl')])
-    def create_from_constructor_without_weights(self, request):
-        expected_qpt_ph_modes = request.param
+    @pytest.mark.parametrize('expected_qpt_ph_modes', [
+        get_expected_qpt_ph_modes('quartz'),
+        get_expected_qpt_ph_modes('NaCl')])
+    def test_create_from_constructor_without_weights(
+            self, expected_qpt_ph_modes):
         args, kwargs = expected_qpt_ph_modes.to_constructor_args(weights=False)
         qpt_ph_modes = QpointPhononModes(*args, **kwargs)
-        return qpt_ph_modes, expected_qpt_ph_modes
+        check_qpt_ph_modes(qpt_ph_modes, expected_qpt_ph_modes,
+                           check_evecs=True)
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('material, phonon_file, json_file', [
         ('LZO', 'La2Zr2O7.phonon',
          'LZO_from_castep_qpoint_phonon_modes.json'),
         ('Si2-sc-skew', 'Si2-sc-skew.phonon',
@@ -186,15 +188,16 @@ def create_from_constructor_without_weights(self, request):
          'quartz_from_castep_qpoint_phonon_modes.json'),
         ('quartz', 'quartz_split_qpts.phonon',
          'quartz_split_from_castep_qpoint_phonon_modes.json')])
-    def create_from_castep(self, request):
-        material, phonon_file, json_file = request.param
+    def test_create_from_castep(self, material, phonon_file, json_file):
         qpt_ph_modes = QpointPhononModes.from_castep(
             get_castep_path(material, phonon_file))
         expected_qpt_ph_modes = ExpectedQpointPhononModes(
             os.path.join(get_qpt_ph_modes_dir(material), json_file))
-        return qpt_ph_modes, expected_qpt_ph_modes
+        check_qpt_ph_modes(qpt_ph_modes, expected_qpt_ph_modes,
+                           check_evecs=True)
 
-    @pytest.fixture(params=[
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize('material, subdir, phonopy_args, json_file', [
         ('NaCl', 'band', {'summary_name': 'should_not_be_read',
                           'phonon_name': 'band.yaml'},
          'NaCl_band_yaml_from_phonopy_qpoint_phonon_modes.json'),
@@ -224,43 +227,31 @@ def create_from_castep(self, request):
         ('CaHgO2', '', {'summary_name': 'mp-7041-20180417.yaml',
                         'phonon_name': 'qpoints.yaml'},
          'CaHgO2_from_phonopy_qpoint_phonon_modes.json')])
-    def create_from_phonopy(self, request):
-        material, subdir, phonopy_args, json_file = request.param
+    def test_create_from_phonopy(self, material, subdir, phonopy_args, json_file):
         phonopy_args['path'] = get_phonopy_path(material, subdir)
         qpt_ph_modes = QpointPhononModes.from_phonopy(**phonopy_args)
         json_path = os.path.join(
             get_qpt_ph_modes_dir(material), json_file)
         expected_qpt_ph_modes = ExpectedQpointPhononModes(json_path)
-        return qpt_ph_modes, expected_qpt_ph_modes
+        check_qpt_ph_modes(qpt_ph_modes, expected_qpt_ph_modes,
+                           check_evecs=True)
 
-    @pytest.fixture(params=['LZO', 'quartz', 'NaCl'])
-    def create_from_json(self, request):
-        material = request.param
+    @pytest.mark.parametrize('material', ['LZO', 'quartz', 'NaCl'])
+    def test_create_from_json(self, material):
         expected_qpt_ph_modes = get_expected_qpt_ph_modes(material)
         qpt_ph_modes = get_qpt_ph_modes(material)
-        return qpt_ph_modes, expected_qpt_ph_modes
+        check_qpt_ph_modes(qpt_ph_modes, expected_qpt_ph_modes,
+                           check_evecs=True)
 
-    @pytest.fixture(params=['LZO', 'quartz', 'NaCl'])
-    def create_from_dict(self, request):
-        material = request.param
+    @pytest.mark.parametrize('material', ['LZO', 'quartz', 'NaCl'])
+    def test_create_from_dict(self, material):
         expected_qpt_ph_modes = get_expected_qpt_ph_modes(material)
         qpt_ph_modes = QpointPhononModes.from_dict(
             expected_qpt_ph_modes.to_dict())
-        return qpt_ph_modes, expected_qpt_ph_modes
-
-    @pytest.mark.parametrize(('qpt_ph_modes_creator'), [
-        pytest.lazy_fixture('create_from_constructor'),
-        pytest.lazy_fixture('create_from_constructor_without_weights'),
-        pytest.lazy_fixture('create_from_dict'),
-        pytest.lazy_fixture('create_from_json'),
-        pytest.lazy_fixture('create_from_castep'),
-        pytest.lazy_fixture('create_from_phonopy')])
-    def test_correct_object_creation(self, qpt_ph_modes_creator):
-        qpt_ph_modes, expected_qpt_ph_modes = qpt_ph_modes_creator
         check_qpt_ph_modes(qpt_ph_modes, expected_qpt_ph_modes,
                            check_evecs=True)
 
-    @pytest.fixture(params=[
+    @pytest.mark.parametrize('faulty_arg, faulty_value, expected_exception', [
         ('qpts',
          get_expected_qpt_ph_modes('quartz').qpts[:3],
          ValueError),
@@ -285,19 +276,14 @@ def test_correct_object_creation(self, qpt_ph_modes_creator):
         ('crystal',
          get_crystal('LZO'),
          ValueError)])
-    def inject_faulty_elements(self, request):
-        faulty_arg, faulty_value, expected_exception = request.param
+    def test_faulty_object_creation(
+            self, faulty_arg, faulty_value, expected_exception):
         expected_qpt_ph_modes = get_expected_qpt_ph_modes('quartz')
         # Inject the faulty value and get a tuple of constructor arguments
         args, kwargs = expected_qpt_ph_modes.to_constructor_args(
             **{faulty_arg: faulty_value})
-        return args, kwargs, expected_exception
-
-    def test_faulty_object_creation(self, inject_faulty_elements):
-        faulty_args, faulty_kwargs, expected_exception = inject_faulty_elements
         with pytest.raises(expected_exception):
-            QpointPhononModes(*faulty_args, **faulty_kwargs)
-
+            QpointPhononModes(*args, **kwargs)
 
     @pytest.mark.parametrize('material, subdir, phonopy_args', [
         ('NaCl', 'qpoints', {'summary_name': 'phonopy.yaml',
@@ -318,6 +304,7 @@ def mocked_import(name, *args, **kwargs):
         with pytest.raises(ImportPhonopyReaderError):
             QpointPhononModes.from_phonopy(**phonopy_args)
 
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('material, subdir, phonopy_args, err', [
         ('NaCl', 'qpoints', {'summary_name': 'phonopy.yaml',
                              'phonon_name': 'qpoints_hdf5.test'},
@@ -345,6 +332,7 @@ def test_create_from_phonopy_with_bad_inputs_raises_err(
         with pytest.raises(err):
             QpointPhononModes.from_phonopy(**phonopy_args)
 
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('material, subdir, phonopy_args, json_file', [
         ('CaHgO2', '', {'summary_name': 'mp-7041-20180417.yaml',
                         'phonon_name': 'qpoints.yaml'},
@@ -369,7 +357,6 @@ def mocked_import(name, globals, locals, fromlist, level):
         check_qpt_ph_modes(qpt_ph_modes, expected_qpt_ph_modes,
                            check_evecs=True)
 
-@pytest.mark.unit
 class TestQpointPhononModesSerialisation:
 
     @pytest.mark.parametrize('qpt_ph_modes', [
@@ -414,7 +401,6 @@ def test_to_qpoint_frequencies(
 
 
 
-@pytest.mark.unit
 class TestQpointPhononModesUnitConversion:
 
     @pytest.mark.parametrize('material, attr, unit_val', [
@@ -432,7 +418,6 @@ def test_incorrect_unit_conversion(self, material, unit_attr,
             setattr(qpt_ph_modes, unit_attr, unit_val)
 
 
-@pytest.mark.unit
 class TestQpointPhononModesSetters:
 
     @pytest.mark.parametrize('material, attr, unit, scale', [
@@ -452,7 +437,6 @@ def test_incorrect_unit_conversion(self, material, attr, unit, err):
             setattr(qpt_ph_modes, attr, new_attr)
 
 
-@pytest.mark.unit
 class TestQpointPhononModesReorderFrequencies:
 
     @pytest.mark.parametrize(
@@ -471,7 +455,6 @@ def test_reorder_frequencies(self, material, qpt_ph_modes_file,
         check_qpt_ph_modes(qpt_ph_modes, expected_qpt_ph_modes)
 
 
-@pytest.mark.unit
 class TestQpointPhononModesCalculateDebyeWaller:
 
     @pytest.mark.parametrize(
@@ -496,43 +479,60 @@ class TestQpointPhononModesCalculateDebyeWaller:
             ('Si2-sc-skew', 'Si2-sc-skew-666-grid.phonon',
                 'Si2-sc-skew_666_300K_symm_debye_waller.json', 300,
                 {'symmetrise': True}),
-            ('CaHgO2', 'CaHgO2-666-grid.yaml',
-                'CaHgO2_666_300K_debye_waller.json', 300,
-                {'symmetrise': False})
         ])
     def test_calculate_debye_waller(self, material, qpt_ph_modes_file,
                                     expected_dw_json, temperature, kwargs):
-        if qpt_ph_modes_file.endswith('.phonon'):
-            qpt_ph_modes = QpointPhononModes.from_castep(
-                get_castep_path(material, qpt_ph_modes_file))
-        else:
-            qpt_ph_modes = QpointPhononModes.from_phonopy(
-                phonon_name=get_phonopy_path(material, qpt_ph_modes_file))
+        qpt_ph_modes = QpointPhononModes.from_castep(
+            get_castep_path(material, qpt_ph_modes_file))
+        dw = qpt_ph_modes.calculate_debye_waller(
+            temperature*ureg('K'), **kwargs)
+        expected_dw = get_expected_dw(material, expected_dw_json)
+        check_debye_waller(dw, expected_dw, dw_atol=1e-12)
 
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize(
+        'material, qpt_ph_modes_file, expected_dw_json, temperature, kwargs', [
+            ('CaHgO2', 'CaHgO2-666-grid.yaml',
+                'CaHgO2_666_300K_debye_waller.json', 300,
+                {'symmetrise': False})
+        ])
+    def test_calculate_debye_waller_from_phonopy(
+            self, material, qpt_ph_modes_file,
+            expected_dw_json, temperature, kwargs):
+        qpt_ph_modes = QpointPhononModes.from_phonopy(
+            phonon_name=get_phonopy_path(material, qpt_ph_modes_file))
         dw = qpt_ph_modes.calculate_debye_waller(
             temperature*ureg('K'), **kwargs)
         expected_dw = get_expected_dw(material, expected_dw_json)
         check_debye_waller(dw, expected_dw, dw_atol=1e-12)
 
 
-@pytest.mark.unit
 class TestQpointPhononModesCalculateDos:
 
     @pytest.mark.parametrize(
         'material, qpt_ph_modes_file, expected_dos_json, ebins', [
             ('quartz', 'quartz-666-grid.phonon',
              'quartz_666_dos.json', np.arange(0, 155, 0.5)*ureg('meV')),
-            ('CaHgO2', 'CaHgO2-666-grid.yaml',
-             'CaHgO2_666_dos.json', np.arange(0, 95, 0.4)*ureg('meV'))
         ])
     def test_calculate_dos(self, material, qpt_ph_modes_file,
                            expected_dos_json, ebins):
-        if qpt_ph_modes_file.endswith('.phonon'):
-            qpt_ph_modes = QpointPhononModes.from_castep(
-                get_castep_path(material, qpt_ph_modes_file))
-        else:
-            qpt_ph_modes = QpointPhononModes.from_phonopy(
-                phonon_name=get_phonopy_path(material, qpt_ph_modes_file))
+        qpt_ph_modes = QpointPhononModes.from_castep(
+            get_castep_path(material, qpt_ph_modes_file))
+        dos = qpt_ph_modes.calculate_dos(ebins)
+        expected_dos = get_expected_spectrum1d(expected_dos_json)
+        check_spectrum1d(dos, expected_dos)
+
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize(
+        'material, qpt_ph_modes_file, expected_dos_json, ebins', [
+            ('CaHgO2', 'CaHgO2-666-grid.yaml',
+             'CaHgO2_666_dos.json', np.arange(0, 95, 0.4)*ureg('meV'))
+        ])
+    def test_calculate_dos_from_phonopy(
+            self, material, qpt_ph_modes_file,
+            expected_dos_json, ebins):
+        qpt_ph_modes = QpointPhononModes.from_phonopy(
+            phonon_name=get_phonopy_path(material, qpt_ph_modes_file))
         dos = qpt_ph_modes.calculate_dos(ebins)
         expected_dos = get_expected_spectrum1d(expected_dos_json)
         check_spectrum1d(dos, expected_dos)
@@ -587,7 +587,6 @@ def test_calculate_dos_with_0_inv_cm_bin_doesnt_raise_runtime_warn(self):
             dos = qpt_ph_modes.calculate_dos(ebins)
         assert len(warn_record) == 0
 
-@pytest.mark.unit
 class TestQpointPhononModesCalculatePdos:
 
     @pytest.mark.parametrize(
@@ -699,7 +698,6 @@ def test_cross_sections_wrong_units_raises_value_error(self):
 
 
 
-@pytest.mark.unit
 class TestQpointPhononModesGetDispersion:
 
     @pytest.mark.parametrize(
diff --git a/tests_and_analysis/test/euphonic_test/test_ref_data.py b/tests_and_analysis/test/euphonic_test/test_ref_data.py
index f2cad0ec1..f3a592052 100644
--- a/tests_and_analysis/test/euphonic_test/test_ref_data.py
+++ b/tests_and_analysis/test/euphonic_test/test_ref_data.py
@@ -7,7 +7,6 @@
 from euphonic.util import get_reference_data
 
 
-@pytest.mark.unit
 class TestReferenceData:
 
     def test_bad_collection(self):
diff --git a/tests_and_analysis/test/euphonic_test/test_spectrum1d.py b/tests_and_analysis/test/euphonic_test/test_spectrum1d.py
index e5044aad2..bfc44de6d 100644
--- a/tests_and_analysis/test/euphonic_test/test_spectrum1d.py
+++ b/tests_and_analysis/test/euphonic_test/test_spectrum1d.py
@@ -116,7 +116,6 @@ def check_spectrum1d(actual_spectrum1d, expected_spectrum1d):
                 == expected_spectrum1d.metadata)
 
 
-@pytest.mark.unit
 class TestSpectrum1DCreation:
 
     # As x_data can be either bin centres or edges, test both cases with
@@ -212,7 +211,6 @@ def test_faulty_object_creation(self, inject_faulty_elements):
             Spectrum1D(*faulty_args, **faulty_kwargs)
 
 
-@pytest.mark.unit
 class TestSpectrum1DSerialisation:
 
     # Note that when writing .text there must be the same number of
@@ -258,7 +256,6 @@ def test_serialise_to_dict(self, json_file):
         check_spectrum1d(spec1d_from_dict, expected_spec1d)
 
 
-@pytest.mark.unit
 class TestSpectrum1DUnitConversion:
 
     @pytest.mark.parametrize('spectrum1d_file, attr, unit_val', [
@@ -278,7 +275,6 @@ def test_incorrect_unit_conversion(self, spectrum1d_file, unit_attr,
             setattr(spec1d, unit_attr, unit_val)
 
 
-@pytest.mark.unit
 class TestSpectrum1DSetters:
 
     @pytest.mark.parametrize('spectrum1d_file, attr, unit, scale', [
@@ -303,7 +299,6 @@ def test_incorrect_unit_conversion(self, spectrum1d_file, attr,
             setattr(spec1d, attr, new_attr)
 
 
-@pytest.mark.unit
 class TestSpectrum1DMethods:
     @pytest.mark.parametrize(
         'args, spectrum1d_file, split_spectrum_files', [
diff --git a/tests_and_analysis/test/euphonic_test/test_spectrum1dcollection.py b/tests_and_analysis/test/euphonic_test/test_spectrum1dcollection.py
index 1ee31ff91..5ef74913c 100644
--- a/tests_and_analysis/test/euphonic_test/test_spectrum1dcollection.py
+++ b/tests_and_analysis/test/euphonic_test/test_spectrum1dcollection.py
@@ -119,7 +119,6 @@ def check_spectrum1dcollection(actual_spectrum, expected_spectrum):
                 == expected_spectrum.metadata)
 
 
-@pytest.mark.unit
 class TestSpectrum1DCollectionCreation:
     @pytest.fixture(params=[
         get_expected_spectrum1dcollection('gan_bands.json'),
@@ -270,7 +269,6 @@ def test_faulty_create_from_sequence(self, input_spectra, expected_error):
             Spectrum1DCollection.from_spectra(input_spectra)
 
 
-@pytest.mark.unit
 class TestSpectrum1DCollectionSerialisation:
 
     # Note that when writing .text there must be the same number of
@@ -321,7 +319,6 @@ def test_serialise_to_dict(self, json_file):
         check_spectrum1dcollection(spectrum_from_dict, expected_spectrum)
 
 
-@pytest.mark.unit
 class TestSpectrum1DCollectionUnitConversion:
 
     @pytest.mark.parametrize('spectrum1d_file, attr, unit_val', [
@@ -341,7 +338,6 @@ def test_incorrect_unit_conversion(self, spectrum1d_file, unit_attr,
             setattr(spec1d, unit_attr, unit_val)
 
 
-@pytest.mark.unit
 class TestSpectrum1DCollectionSetters:
 
     @pytest.mark.parametrize('spectrum1d_file, attr, unit, scale', [
@@ -366,7 +362,6 @@ def test_incorrect_unit_conversion(self, spectrum1d_file, attr,
             setattr(spec1d, attr, new_attr)
 
 
-@pytest.mark.unit
 class TestSpectrum1DCollectionIndexAccess:
     @pytest.mark.parametrize(
         'spectrum, index, expected_spectrum1d',
@@ -422,7 +417,6 @@ def test_index_errors(self, spectrum, index, expected_error):
             spectrum[index]
 
 
-@pytest.mark.unit
 class TestSpectrum1DCollectionMethods:
     @pytest.mark.parametrize(
         'spectrum, split_kwargs, expected_spectra',
diff --git a/tests_and_analysis/test/euphonic_test/test_spectrum2d.py b/tests_and_analysis/test/euphonic_test/test_spectrum2d.py
index 7c57d1854..bd3c605ef 100644
--- a/tests_and_analysis/test/euphonic_test/test_spectrum2d.py
+++ b/tests_and_analysis/test/euphonic_test/test_spectrum2d.py
@@ -129,7 +129,6 @@ def check_spectrum2d(actual_spectrum2d, expected_spectrum2d):
                 == expected_spectrum2d.metadata)
 
 
-@pytest.mark.unit
 class TestSpectrum2DCreation:
 
     # As x_data and y_data can be either bin centres or edges, test all
@@ -219,7 +218,6 @@ def test_faulty_object_creation(self, inject_faulty_elements):
             Spectrum2D(*faulty_args, **faulty_kwargs)
 
 
-@pytest.mark.unit
 class TestSpectrum2DSerialisation:
 
     @pytest.mark.parametrize('spec2d', [
@@ -253,7 +251,6 @@ def test_serialise_to_dict(self, serialise_to_dict):
         check_spectrum2d(spec2d, expected_spec2d)
 
 
-@pytest.mark.unit
 class TestSpectrum2DUnitConversion:
 
     @pytest.mark.parametrize('spectrum2d_file, attr, unit_val', [
@@ -275,7 +272,6 @@ def test_incorrect_unit_conversion(self, spectrum2d_file, unit_attr,
             setattr(spec2d, unit_attr, unit_val)
 
 
-@pytest.mark.unit
 class TestSpectrum2DSetters:
 
     @pytest.mark.parametrize('spectrum2d_file, attr, unit, scale', [
@@ -302,7 +298,6 @@ def test_incorrect_unit_conversion(self, spectrum2d_file, attr,
             setattr(spec2d, attr, new_attr)
 
 
-@pytest.mark.unit
 class TestSpectrum2DMethods:
     @pytest.mark.parametrize(
         'args, spectrum2d_file, split_spectrum_files',
diff --git a/tests_and_analysis/test/euphonic_test/test_structure_factor.py b/tests_and_analysis/test/euphonic_test/test_structure_factor.py
index bb7c103e5..6544c6d36 100644
--- a/tests_and_analysis/test/euphonic_test/test_structure_factor.py
+++ b/tests_and_analysis/test/euphonic_test/test_structure_factor.py
@@ -183,7 +183,6 @@ def check_structure_factor(
         gamma_atol=sf_gamma_atol)
 
 
-@pytest.mark.unit
 class TestStructureFactorCreation:
 
     @pytest.fixture(params=[
@@ -287,7 +286,6 @@ def test_faulty_creation(self, inject_faulty_elements):
             StructureFactor(*faulty_args, **faulty_kwargs)
 
 
-@pytest.mark.unit
 class TestStructureFactorSerialisation:
 
     @pytest.mark.parametrize('sf', [
@@ -331,7 +329,6 @@ def test_to_qpoint_frequencies(
         check_qpt_freqs(qpt_freqs, expected_qpt_freqs)
 
 
-@pytest.mark.unit
 class TestStructureFactorUnitConversion:
 
     @pytest.mark.parametrize('material, json_file, attr, unit_val', [
@@ -360,7 +357,6 @@ def test_incorrect_unit_conversion(self, material, json_file, unit_attr,
             setattr(sf, unit_attr, unit_val)
 
 
-@pytest.mark.unit
 class TestStructureFactorSetters:
 
     @pytest.mark.parametrize('material, json_file, attr, unit, scale', [
@@ -395,7 +391,6 @@ def test_incorrect_unit_conversion(self, material, json_file, attr,
             setattr(sf, attr, new_attr)
 
 
-@pytest.mark.unit
 class TestStructureFactorCalculateSqwMap:
 
     @pytest.mark.parametrize(
@@ -443,7 +438,6 @@ def test_inconsistent_temperatures_raises_error(
         with pytest.raises(err):
             sf.calculate_sqw_map(ebins, **kwargs)
 
-@pytest.mark.unit
 class TestStructureFactorCalculate1dAverage:
 
     @pytest.mark.parametrize(
@@ -467,7 +461,6 @@ def test_calculate_1d_average(self, material, sf_json, expected_1d_json,
         expected_sw = get_expected_spectrum1d(expected_1d_json)
         check_spectrum1d(sw, expected_sw)
 
-@pytest.mark.unit
 class TestStructureFactorGetDispersion:
 
     @pytest.mark.parametrize(
@@ -485,7 +478,6 @@ def test_get_dispersion(
             expected_dispersion_json)
         check_spectrum1dcollection(disp, expected_disp)
 
-@pytest.mark.unit
 class TestStructureFactorCalculateDos:
 
     @pytest.mark.parametrize(
diff --git a/tests_and_analysis/test/euphonic_test/test_util.py b/tests_and_analysis/test/euphonic_test/test_util.py
index 5793f85c9..3704fddc0 100644
--- a/tests_and_analysis/test/euphonic_test/test_util.py
+++ b/tests_and_analysis/test/euphonic_test/test_util.py
@@ -14,7 +14,6 @@
     get_fc_dir)
 
 
-@pytest.mark.unit
 class TestDirectionChanged:
 
     def test_direction_changed_nah(self):
@@ -31,7 +30,6 @@ def test_direction_changed_nah(self):
                          expected_direction_changed)
 
 
-@pytest.mark.unit
 class TestMPGrid:
 
     def test_444_grid(self):
@@ -41,7 +39,6 @@ def test_444_grid(self):
         npt.assert_equal(qpts, expected_qpts)
 
 
-@pytest.mark.unit
 class TestGetQptLabels:
 
     @pytest.mark.parametrize('qpts, kwargs, expected_labels', [
@@ -80,7 +77,6 @@ def get_modg_norm(mode_gradients_file):
     return np.linalg.norm(modg.magnitude, axis=-1)*modg.units
 
 
-@pytest.mark.unit
 class TestModeGradientsToWidths:
 
     @pytest.mark.parametrize('mode_grads, cell_vecs, expected_mode_widths_file', [
diff --git a/tests_and_analysis/test/pytest.ini b/tests_and_analysis/test/pytest.ini
index 4f2f36cc7..c412199bc 100644
--- a/tests_and_analysis/test/pytest.ini
+++ b/tests_and_analysis/test/pytest.ini
@@ -1,4 +1,4 @@
 [pytest]
 markers =
-    unit: mark tests as a unit test
-    integration: mark tests as an integration test
+    phonopy_reader: requires euphonic[phonopy_reader] extra to be installed
+    matplotlib: requires euphonic[matplotlib] extra to be installed
diff --git a/tests_and_analysis/test/run_tests.py b/tests_and_analysis/test/run_tests.py
index 688d77f9a..c52c9b881 100644
--- a/tests_and_analysis/test/run_tests.py
+++ b/tests_and_analysis/test/run_tests.py
@@ -1,9 +1,27 @@
+import argparse
 import sys
 import os
+import time
+from typing import Tuple, List, Union
+
 import pytest
 import coverage
-import argparse
-from typing import Tuple, List, Union
+
+
+def main():
+    test_dir, reports_dir = _get_test_and_reports_dir()
+
+    (do_report_coverage, do_report_tests, tests,
+     markers_to_run) = _get_parsed_args(test_dir)
+
+    pytest_options: List[str] = _build_pytest_options(
+        reports_dir, do_report_tests, tests, markers_to_run)
+
+    test_exit_code: int = run_tests(
+        pytest_options, do_report_coverage, reports_dir, test_dir)
+
+    # Exit with a failure code if there are any errors or failures
+    sys.exit(test_exit_code)
 
 
 def _get_test_and_reports_dir() -> Tuple[str, str]:
@@ -47,7 +65,7 @@ def _get_parsed_args(test_dir: str) -> Tuple[bool, bool, str, str]:
     parser = argparse.ArgumentParser()
     parser.add_argument(
         "--cov", action="store_true",
-        help="If present report coverage in a coverage.xml file in reports")
+        help="If present report coverage in a coverage*.xml file in reports")
     parser.add_argument(
         "--report", action="store_true",
         help="If present report test results to junit_report*.xml files")
@@ -62,8 +80,8 @@ def _get_parsed_args(test_dir: str) -> Tuple[bool, bool, str, str]:
             args_parsed.markers_to_run)
 
 
-def _build_pytest_options(reports_dir: str, do_report_tests: bool, tests: str,
-                          markers: str) -> List[str]:
+def _build_pytest_options(reports_dir: str, do_report_tests: bool,
+                          tests: str, markers: str) -> List[str]:
     """
     Build the options for pytest to use.
 
@@ -87,17 +105,14 @@ def _build_pytest_options(reports_dir: str, do_report_tests: bool, tests: str,
     options: List[str] = [tests]
     # Add reporting of test results
     if do_report_tests:
-        # We may have multiple reports, so do not overwrite them
+        # We may have multiple reports, so get a unique filename
         filename_prefix = "junit_report"
-        filenum = 0
-        for filename in os.listdir(reports_dir):
-            if filename_prefix in filename:
-                filenum += 1
-        junit_xml_filepath: str = os.path.join(
-            test_dir, "reports", "{}{}.xml".format(filename_prefix, filenum))
-        options.append("--junitxml={}".format(junit_xml_filepath))
+        filenum = int(time.time())
+        junit_xml_filepath = os.path.join(
+            reports_dir, f"{filename_prefix}_{filenum}.xml")
+        options.append(f"--junitxml={junit_xml_filepath}")
     # Only run the specified markers
-    options.append("-m={}".format(markers))
+    options.append(f"-m={markers}")
     return options
 
 
@@ -111,7 +126,7 @@ def run_tests(pytest_options: List[str], do_report_coverage: bool,
     pytest_options : List[str]
         The options to pass to pytest
     do_report_coverage : bool
-        If true report coverage to coverage.xml
+        If true report coverage to coverage*.xml
     reports_dir : str
         The directory to report coverage to
     test_dir: str
@@ -142,23 +157,12 @@ def run_tests(pytest_options: List[str], do_report_coverage: bool,
     # Report coverage if requested
     if do_report_coverage and cov is not None:
         cov.stop()
-        coverage_xml_filepath = os.path.join(reports_dir, "coverage.xml")
+        coverage_xml_filepath = os.path.join(
+            reports_dir, f"coverage_{int(time.time())}.xml")
         cov.xml_report(outfile=coverage_xml_filepath)
 
     return test_exit_code
 
 
 if __name__ == "__main__":
-    test_dir, reports_dir = _get_test_and_reports_dir()
-
-    do_report_coverage, do_report_tests, tests, markers_to_run = \
-        _get_parsed_args(test_dir)
-
-    pytest_options: List[str] = _build_pytest_options(
-        reports_dir, do_report_tests, tests, markers_to_run)
-
-    test_exit_code: int = \
-        run_tests(pytest_options, do_report_coverage, reports_dir, test_dir)
-
-    # Exit with a failure code if there are any errors or failures
-    sys.exit(test_exit_code)
+    main()
diff --git a/tests_and_analysis/test/script_tests/test_dispersion.py b/tests_and_analysis/test/script_tests/test_dispersion.py
index 1083ac520..2bcdcf372 100644
--- a/tests_and_analysis/test/script_tests/test_dispersion.py
+++ b/tests_and_analysis/test/script_tests/test_dispersion.py
@@ -6,8 +6,6 @@
 
 import pytest
 import numpy.testing as npt
-# Required for mocking
-import matplotlib.pyplot
 from packaging import version
 from scipy import __version__ as scipy_ver
 
@@ -15,8 +13,15 @@
     get_data_path, get_castep_path, get_phonopy_path)
 from tests_and_analysis.test.script_tests.utils import (
     get_script_test_data_path, get_current_plot_line_data, args_to_key)
-import euphonic.cli.dispersion
 
+pytestmark = pytest.mark.matplotlib
+# Allow tests with matplotlib marker to be collected and
+# deselected if Matplotlib is not installed
+try:
+    import matplotlib.pyplot
+    import euphonic.cli.dispersion
+except ModuleNotFoundError:
+    pass
 
 cahgo2_fc_file = get_phonopy_path('CaHgO2', 'mp-7041-20180417.yaml')
 lzo_fc_file = os.path.join(
@@ -31,6 +36,10 @@
     'quartz_bandstructure_qpoint_phonon_modes.json')
 disp_output_file = os.path.join(get_script_test_data_path(), 'dispersion.json')
 disp_params =  [
+    [lzo_fc_file],
+    [quartz_phonon_file],
+    [quartz_phonon_file, '--btol=1000']]
+disp_params_from_phonopy =  [
     [cahgo2_fc_file],
     [cahgo2_fc_file, '--energy-unit=hartree'],
     [cahgo2_fc_file, '--x-label=wavenumber', '--y-label=Energy (meV)',
@@ -41,16 +50,12 @@
     [cahgo2_fc_file, '--q-spacing=0.02'],
     [cahgo2_fc_file, '--asr'],
     [cahgo2_fc_file, '--asr=realspace'],
-    [lzo_fc_file],
     [nacl_fc_file],
-    [quartz_phonon_file],
-    [quartz_phonon_file, '--btol=1000'],
     [nacl_phonon_file],
     [nacl_phonon_hdf5_file]]
 disp_params_macos_segfault =  [[cahgo2_fc_file, '--reorder']]
 
 
-@pytest.mark.integration
 class TestRegression:
 
     @pytest.fixture
@@ -93,6 +98,13 @@ def run_dispersion_and_test_result(self, dispersion_args):
     def test_dispersion_plot_data(self, inject_mocks, dispersion_args):
         self.run_dispersion_and_test_result(dispersion_args)
 
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize('dispersion_args', disp_params_from_phonopy)
+    def test_dispersion_plot_data_from_phonopy(
+            self, inject_mocks, dispersion_args):
+        self.run_dispersion_and_test_result(dispersion_args)
+
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('dispersion_args', disp_params_macos_segfault)
     @pytest.mark.skipif(
         (any([s in platform() for s in ['Darwin', 'macOS']])
diff --git a/tests_and_analysis/test/script_tests/test_dos.py b/tests_and_analysis/test/script_tests/test_dos.py
index db9170493..f0dcd99b2 100644
--- a/tests_and_analysis/test/script_tests/test_dos.py
+++ b/tests_and_analysis/test/script_tests/test_dos.py
@@ -6,16 +6,21 @@
 import pytest
 import numpy as np
 import numpy.testing as npt
-# Required for mocking
-import matplotlib.pyplot
 
 from tests_and_analysis.test.utils import get_data_path, get_castep_path
 from tests_and_analysis.test.script_tests.utils import (
     get_script_test_data_path, get_current_plot_line_data,
     args_to_key)
 from euphonic.cli.utils import _get_pdos_weighting
-import euphonic.cli.dos
 
+pytestmark = pytest.mark.matplotlib
+# Allow tests with matplotlib marker to be collected and
+# deselected if Matplotlib is not installed
+try:
+    import matplotlib.pyplot
+    import euphonic.cli.dos
+except ModuleNotFoundError:
+    pass
 
 nah_phonon_file = get_castep_path('NaH', 'NaH.phonon')
 quartz_fc_file = get_castep_path('quartz', 'quartz.castep_bin')
@@ -40,7 +45,6 @@
     [quartz_fc_file, '--grid', '5', '5', '4', '--adaptive', '--eb', '2']]
 
 
-@pytest.mark.integration
 class TestRegression:
 
     @pytest.fixture
diff --git a/tests_and_analysis/test/script_tests/test_intensity_map.py b/tests_and_analysis/test/script_tests/test_intensity_map.py
index 54a343185..1817021d6 100644
--- a/tests_and_analysis/test/script_tests/test_intensity_map.py
+++ b/tests_and_analysis/test/script_tests/test_intensity_map.py
@@ -5,16 +5,21 @@
 
 import pytest
 import numpy.testing as npt
-# Required for mocking
-import matplotlib.pyplot
 from packaging import version
 from scipy import __version__ as scipy_ver
 
 from tests_and_analysis.test.utils import get_data_path, get_castep_path
 from tests_and_analysis.test.script_tests.utils import (
     get_script_test_data_path, get_current_plot_image_data, args_to_key)
-import euphonic.cli.intensity_map
 
+pytestmark = pytest.mark.matplotlib
+# Allow tests with matplotlib marker to be collected and
+# deselected if Matplotlib is not installed
+try:
+    import matplotlib.pyplot
+    import euphonic.cli.intensity_map
+except ModuleNotFoundError:
+    pass
 
 quartz_phonon_file = os.path.join(
     get_data_path(), 'qpoint_phonon_modes', 'quartz',
@@ -46,7 +51,6 @@
     [graphite_fc_file, '--weighting=coherent', '--temperature=800']]
 
 
-@pytest.mark.integration
 class TestRegression:
 
     @pytest.fixture
diff --git a/tests_and_analysis/test/script_tests/test_optimise_dipole_parameter.py b/tests_and_analysis/test/script_tests/test_optimise_dipole_parameter.py
index d06b6b304..9850c1eb7 100644
--- a/tests_and_analysis/test/script_tests/test_optimise_dipole_parameter.py
+++ b/tests_and_analysis/test/script_tests/test_optimise_dipole_parameter.py
@@ -20,7 +20,6 @@
 quick_calc_params = ['-n=10', '--min=0.5', '--max=0.5']
 
 
-@pytest.mark.integration
 class TestRegression:
 
     def test_default_dipole_parameters(self):
@@ -46,6 +45,7 @@ def test_called_without_errors(self, optimise_dipole_parameter_args):
         euphonic.cli.optimise_dipole_parameter.main(
             optimise_dipole_parameter_args)
 
+    @pytest.mark.phonopy_reader
     def test_reading_nacl_default_reads_born(self, recwarn):
         # BORN should be read by default so no warning should
         # be raised
diff --git a/tests_and_analysis/test/script_tests/test_powder_map.py b/tests_and_analysis/test/script_tests/test_powder_map.py
index f1aa2e5fd..becf916af 100644
--- a/tests_and_analysis/test/script_tests/test_powder_map.py
+++ b/tests_and_analysis/test/script_tests/test_powder_map.py
@@ -5,15 +5,21 @@
 
 import pytest
 import numpy.testing as npt
-# Required for mocking
-import matplotlib.pyplot
 from packaging import version
 from scipy import __version__ as scipy_ver
 
 from tests_and_analysis.test.utils import get_data_path, get_castep_path, get_phonopy_path
 from tests_and_analysis.test.script_tests.utils import (
     get_script_test_data_path, get_current_plot_image_data, args_to_key)
-import euphonic.cli.powder_map
+
+pytestmark = pytest.mark.matplotlib
+# Allow tests with matplotlib marker to be collected and
+# deselected if Matplotlib is not installed
+try:
+    import matplotlib.pyplot
+    import euphonic.cli.powder_map
+except ModuleNotFoundError:
+    pass
 
 graphite_fc_file = get_castep_path('graphite', 'graphite.castep_bin')
 nacl_prim_fc_file = get_phonopy_path('NaCl_prim', 'phonopy_nacl.yaml')
@@ -22,12 +28,6 @@
 
 quick_calc_params = ['--npts=10', '--npts-min=10', '--q-spacing=1']
 powder_map_params = [
-    [nacl_prim_fc_file],
-    [nacl_prim_fc_file, '--temperature=1000', *quick_calc_params],
-    [nacl_prim_fc_file, '--v-min=0', '--v-max=1e-10', *quick_calc_params],
-    [nacl_prim_fc_file, '--energy-unit=meV', *quick_calc_params],
-    [nacl_prim_fc_file, '--weighting=coherent', '--c-map=bone',
-     *quick_calc_params],
     [graphite_fc_file, '-w', 'dos', '--y-label=DOS', '--title=DOS TITLE',
      *quick_calc_params],
     [graphite_fc_file, '--e-min=50', '-u=cm^-1', '--x-label=wavenumber',
@@ -42,6 +42,13 @@
      *quick_calc_params],
     [graphite_fc_file, '--asr', *quick_calc_params],
     [graphite_fc_file, '--asr=realspace', '--dipole-parameter=0.75',
+     *quick_calc_params]]
+powder_map_params_from_phonopy = [
+    [nacl_prim_fc_file],
+    [nacl_prim_fc_file, '--temperature=1000', *quick_calc_params],
+    [nacl_prim_fc_file, '--v-min=0', '--v-max=1e-10', *quick_calc_params],
+    [nacl_prim_fc_file, '--energy-unit=meV', *quick_calc_params],
+    [nacl_prim_fc_file, '--weighting=coherent', '--c-map=bone',
      *quick_calc_params],
     [nacl_prim_fc_file, '-w=incoherent-dos', '--pdos=Na', '--no-widget',
      *quick_calc_params],
@@ -54,7 +61,6 @@
      *quick_calc_params]]
 
 
-@pytest.mark.integration
 class TestRegression:
 
     @pytest.fixture
@@ -99,6 +105,14 @@ def test_powder_map_plot_image(
             self, inject_mocks, powder_map_args):
         self.run_powder_map_and_test_result(powder_map_args)
 
+    @pytest.mark.phonopy_reader
+    @pytest.mark.parametrize(
+        'powder_map_args', powder_map_params_from_phonopy)
+    def test_powder_map_plot_image_from_phonopy(
+            self, inject_mocks, powder_map_args):
+        self.run_powder_map_and_test_result(powder_map_args)
+
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('powder_map_args', powder_map_params_macos_segfault)
     @pytest.mark.skipif(
         (any([s in platform() for s in ['Darwin', 'macOS']])
@@ -109,6 +123,7 @@ def test_powder_map_plot_image_macos_segfault(
             self, inject_mocks, powder_map_args):
         self.run_powder_map_and_test_result(powder_map_args)
 
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('powder_map_args', [
         [nacl_prim_fc_file, '--save-to'],
         [nacl_prim_fc_file, '-s']])
@@ -124,6 +139,7 @@ def test_invalid_file_raises_value_error(self, powder_map_args):
         with pytest.raises(ValueError):
             euphonic.cli.powder_map.main(powder_map_args)
 
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('powder_map_args', [
         [nacl_prim_fc_file, '--weights=dos']])
     def test_weights_emits_deprecation_warning(
@@ -140,6 +156,7 @@ def test_invalid_weighting_raises_causes_exit(self, powder_map_args):
         assert err.type == SystemExit
         assert err.value.code == 2
 
+    @pytest.mark.phonopy_reader
     @pytest.mark.parametrize('powder_map_args', [
         [nacl_prim_fc_file, '-w=coherent', '--pdos', 'Na']])
     def test_coherent_weighting_and_pdos_raises_value_error(
diff --git a/tests_and_analysis/test/script_tests/test_sphere_sampling.py b/tests_and_analysis/test/script_tests/test_sphere_sampling.py
index 98b3c0f0b..50dac0793 100644
--- a/tests_and_analysis/test/script_tests/test_sphere_sampling.py
+++ b/tests_and_analysis/test/script_tests/test_sphere_sampling.py
@@ -6,13 +6,18 @@
 import pytest
 import numpy as np
 import numpy.testing as npt
-# Required for mocking
-import matplotlib.pyplot
 
 from tests_and_analysis.test.script_tests.utils import (
     get_script_test_data_path, get_current_plot_offsets)
-import euphonic.cli.show_sampling
 
+pytestmark = pytest.mark.matplotlib
+# Allow tests with matplotlib marker to be collected and
+# deselected if Matplotlib is not installed
+try:
+    import matplotlib.pyplot
+    import euphonic.cli.show_sampling
+except ModuleNotFoundError:
+    pass
 
 sphere_sampling_output_file = os.path.join(get_script_test_data_path(),
                                            "sphere_sampling.json")
@@ -32,7 +37,6 @@
     ['10', 'random-sphere', '--jitter']]
 
 
-@pytest.mark.integration
 class TestRegression:
 
     @pytest.fixture
diff --git a/tests_and_analysis/test/script_tests/test_styling.py b/tests_and_analysis/test/script_tests/test_styling.py
index cb626a24f..dfd7e23cc 100644
--- a/tests_and_analysis/test/script_tests/test_styling.py
+++ b/tests_and_analysis/test/script_tests/test_styling.py
@@ -1,14 +1,21 @@
 from argparse import Namespace
 
 import pytest
-import matplotlib.pyplot
 from numpy.testing import assert_allclose
 
-import euphonic.cli.dos
 from euphonic.cli.utils import _compose_style
 
 from tests_and_analysis.test.utils import get_castep_path
 
+pytestmark = pytest.mark.matplotlib
+# Allow tests with matplotlib marker to be collected and
+# deselected if Matplotlib is not installed
+try:
+    import matplotlib.pyplot
+    import euphonic.cli.dos
+except ModuleNotFoundError:
+    pass
+
 compose_style_cases = [
     ({'user_args': Namespace(unused=1, no_base_style=False, style=None),
       'base': None}, [{}]),
@@ -37,14 +44,12 @@
     ]
 
 
-@pytest.mark.unit
 @pytest.mark.parametrize('kwargs,expected_style', compose_style_cases)
 def test_compose_style(kwargs, expected_style):
     """Internal function which interprets matplotlib style options"""
     assert _compose_style(**kwargs) == expected_style
 
 
-@pytest.mark.integration
 class TestDOSStyling:
 
     @pytest.fixture
diff --git a/tests_and_analysis/test/script_tests/utils.py b/tests_and_analysis/test/script_tests/utils.py
index b3c36dc09..5952a5238 100644
--- a/tests_and_analysis/test/script_tests/utils.py
+++ b/tests_and_analysis/test/script_tests/utils.py
@@ -1,7 +1,9 @@
 import os
 # Required for mocking
-import matplotlib.pyplot
-from matplotlib.axes import Axes
+try:
+    import matplotlib.pyplot
+except ModuleNotFoundError:
+    pass
 from typing import Dict, List, Union
 from ..utils import get_data_path
 
@@ -97,8 +99,8 @@ def get_current_plot_image_data() -> Dict[str,
     return data
 
 
-def get_ax_image_data(ax: Axes) -> Dict[str,
-                                        Union[str, List[float], List[int]]]:
+def get_ax_image_data(ax: 'matplotlib.axes.Axes'
+                      ) -> Dict[str, Union[str, List[float], List[int]]]:
     im = ax.get_images()[0]
     # Convert negative zero to positive zero
     im_data = im.get_array()