Skip to content

Commit

Permalink
Organise unit tests to use pytest fixtures
Browse files Browse the repository at this point in the history
Unit tests have an excessive amount of repeated test setup code, making
unit tests harder to maintain and read. Using pytest fixtures to setup
each test would remove needless code repetition and also prevents the
possibility of state leaking into each test case.

Organise the tests so that for each function `func`, we have a test
class `TestFunc` with each method of `TestFunc` containing a single test
(success or failure case).

Use pytest's `parametrize()` feature for testing different levels of
verbosity.

Remove usage of `mock_cwd` fixture unless it is required for a test to
pass.

Fixes #163
  • Loading branch information
SeanBryan51 committed Oct 12, 2023
1 parent fdcab39 commit 107cc86
Show file tree
Hide file tree
Showing 11 changed files with 1,630 additions and 1,437 deletions.
118 changes: 0 additions & 118 deletions tests/common.py

This file was deleted.

152 changes: 141 additions & 11 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,156 @@

import os
import shutil
import tempfile
from pathlib import Path
from subprocess import CalledProcessError, CompletedProcess
from typing import Optional

import pytest

from .common import MOCK_CWD
from benchcab.environment_modules import EnvironmentModulesInterface
from benchcab.utils.subprocess import SubprocessWrapperInterface


@pytest.fixture()
def mock_cwd():
"""Create and return a unique temporary directory to use as the CWD.
The return value is the path of the directory.
"""
return Path(tempfile.mkdtemp(prefix="benchcab_tests"))


@pytest.fixture(autouse=True)
def _run_around_tests():
"""`pytest` autouse fixture that runs around each test."""
# Setup:
def _run_around_tests(mock_cwd):
"""Change into the `mock_cwd` directory."""
prevdir = Path.cwd()
if MOCK_CWD.exists():
shutil.rmtree(MOCK_CWD)
MOCK_CWD.mkdir()
os.chdir(MOCK_CWD.expanduser())
os.chdir(mock_cwd.expanduser())

# Run the test:
yield

# Teardown:
os.chdir(prevdir)
shutil.rmtree(MOCK_CWD)
shutil.rmtree(mock_cwd)


@pytest.fixture()
def config():
"""Returns a valid mock config."""
return {
"project": "bar",
"experiment": "five-site-test",
"modules": [
"intel-compiler/2021.1.1",
"openmpi/4.1.0",
"netcdf/4.7.4",
],
"realisations": [
{
"name": "trunk",
"revision": 9000,
"path": "trunk",
"patch": {},
"patch_remove": {},
"build_script": "",
},
{
"name": "v3.0-YP-changes",
"revision": -1,
"path": "branches/Users/sean/my-branch",
"patch": {"cable": {"cable_user": {"ENABLE_SOME_FEATURE": False}}},
"patch_remove": {},
"build_script": "",
},
],
"science_configurations": [
{
"cable": {
"cable_user": {
"GS_SWITCH": "medlyn",
"FWSOIL_SWITCH": "Haverd2013",
}
}
},
{
"cable": {
"cable_user": {
"GS_SWITCH": "leuning",
"FWSOIL_SWITCH": "Haverd2013",
}
}
},
],
"fluxsite": {
"pbs": {
"ncpus": 16,
"mem": "64G",
"walltime": "01:00:00",
"storage": ["gdata/foo123"],
},
"multiprocessing": True,
},
}


# Global string literal used so that it is accessible in tests
DEFAULT_STDOUT = "mock standard output"


@pytest.fixture()
def mock_subprocess_handler():
"""Returns a mock implementation of `SubprocessWrapperInterface`."""

class MockSubprocessWrapper(SubprocessWrapperInterface):
"""A mock implementation of `SubprocessWrapperInterface` used for testing."""

def __init__(self) -> None:
self.commands: list[str] = []
self.stdout = DEFAULT_STDOUT
self.error_on_call = False
self.env = {}

def run_cmd(
self,
cmd: str,
capture_output: bool = False,
output_file: Optional[Path] = None,
verbose: bool = False,
env: Optional[dict] = None,
) -> CompletedProcess:
self.commands.append(cmd)
if self.error_on_call:
raise CalledProcessError(returncode=1, cmd=cmd, output=self.stdout)
if output_file:
output_file.touch()
if env:
self.env = env
return CompletedProcess(cmd, returncode=0, stdout=self.stdout)

return MockSubprocessWrapper()


@pytest.fixture()
def mock_environment_modules_handler():
"""Returns a mock implementation of `EnvironmentModulesInterface`."""

class MockEnvironmentModules(EnvironmentModulesInterface):
"""A mock implementation of `EnvironmentModulesInterface` used for testing."""

def __init__(self) -> None:
self.commands: list[str] = []

def module_is_avail(self, *args: str) -> bool:
self.commands.append("module is-avail " + " ".join(args))
return True

Check warning on line 145 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L144-L145

Added lines #L144 - L145 were not covered by tests

def module_is_loaded(self, *args: str) -> bool:
self.commands.append("module is-loaded " + " ".join(args))
return True

Check warning on line 149 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L148-L149

Added lines #L148 - L149 were not covered by tests

def module_load(self, *args: str) -> None:
self.commands.append("module load " + " ".join(args))

def module_unload(self, *args: str) -> None:
self.commands.append("module unload " + " ".join(args))

return MockEnvironmentModules()
Loading

0 comments on commit 107cc86

Please sign in to comment.