Fixtures
Several pytest
fixtures are provided to help with testing.
Keepable temporary directories
Tests often need to exercise code that reads from and/or writes to disk. The test harness may also need to create test data during setup and clean up the filesystem on teardown. Temporary directories are built into pytest
via the tmp_path
and tmp_path_factory
fixtures, however the default temporary directory location varies across platforms and may be inconvenient to access.
modflow-devtools
provides a set of fixtures to extend default pytest
temporary directory fixtures’ behavior with the ability to optionally save test outputs in a location of the user’s choice:
function_tmpdir
module_tmpdir
class_tmpdir
session_tmpdir
When pytest
is invoked with a --keep <path>
option, files created by tests using any of the above fixtures are saved under the specified path, in subdirectories named according to the test function.
The fixtures are named according to their scope, and are automatically created before test code runs and lazily removed afterwards, subject to the same cleanup procedure used by the default pytest
temporary directory fixtures. Functionally they are identical to the pytest
-provided fixtures save for the behavior described above.
from pathlib import Path
import inspect
def test_tmpdirs(function_tmpdir, module_tmpdir):
# function-scoped temporary directory
assert function_tmpdir.is_dir()
assert inspect.currentframe().f_code.co_name in function_tmpdir.stem
# module-scoped temp dir (accessible to other tests in the script)
assert module_tmpdir.is_dir()
with open(function_tmpdir / "test.txt", "w") as f1, open(module_tmpdir / "test.txt", "w") as f2:
f1.write("hello, function")
f2.write("hello, module")
There is also a --keep-failed <path>
option which preserves outputs only from failing test cases. Note that this variant is only compatible with function_tmpdir
.
Loading example models
Fixtures are provided to find models from the MODFLOW 6 example and test model repositories and feed them to test functions. Models can be loaded from:
These models can be requested like any other pytest
fixture, by adding one of the following parameters to test functions:
test_model_mf5to6
: aPath
to a MODFLOW 2005 model namefile, loaded from themf5to6
subdirectory of themodflow6-testmodels
repositorytest_model_mf6
: aPath
to a MODFLOW 6 model namefile, loaded from themf6
subdirectory of themodflow6-testmodels
repositorylarge_test_model
: aPath
to a large MODFLOW 6 model namefile, loaded from themodflow6-largetestmodels
repositoryexample_scenario
: aTuple[str, List[Path]]
containing the name of a MODFLOW 6 example scenario and a list of paths to its model namefiles, loaded from themodflow6-examples
repository
See the installation docs for more information on installing test model repositories.
Configuration
It is recommended to set the environment variable REPOS_PATH
to the location of the model repositories on the filesystem. Model repositories must live side-by-side in this location, and repository directories are expected to be named identically to GitHub repositories (the directory may have a .git
suffix). If REPOS_PATH
is not configured, modflow-devtools
assumes tests are being run from an autotest
subdirectory of the consuming project’s root, and model repos live side-by-side with the consuming project. If this guess is incorrect and repositories cannot be found, tests requesting these fixtures will be skipped.
Note: by default, all models found in the respective external repository will be returned by these fixtures. It is up to the consuming project to exclude models if needed. This can be accomplished by:
Usage
MODFLOW 2005 test models
The test_model_mf5to6
fixture are each a Path
to the model’s namefile. For example, to load mf5to6
models from the MODFLOW-USGS/modflow6-testmodels
repo:
def test_mf5to6_model(test_model_mf5to6):
assert isinstance(test_model_mf5to6, Path)
assert test_model_mf5to6.is_file()
assert test_model_mf5to6.suffix == ".nam"
This test function will be parametrized with all models found in the mf5to6
subdirectory of the MODFLOW-USGS/modflow6-testmodels
repository. Note that MODFLOW-2005 namefiles need not be named mfsim.nam
.
MODFLOW 6 test models
The test_model_mf6
fixture loads all MODFLOW 6 models found in the mf6
subdirectory of the MODFLOW-USGS/modflow6-testmodels
repository.
def test_test_model_mf6(test_model_mf6):
assert isinstance(test_model_mf6, Path)
assert test_model_mf6.is_file()
assert test_model_mf6.name == "mfsim.nam"
Because these are MODFLOW 6 models, each namefile will be named mfsim.nam
. The model name can be inferred from the namefile’s parent directory.
Large test models
The large_test_model
fixture loads all MODFLOW 6 models found in the MODFLOW-USGS/modflow6-largetestmodels
repository.
def test_large_test_model(large_test_model):
print(large_test_model)
assert isinstance(large_test_model, Path)
assert large_test_model.is_file()
assert large_test_model.name == "mfsim.nam"
Example scenarios
The MODFLOW-USGS/modflow6-examples
repository contains a collection of example scenarios, each with 1 or more models. The example_scenario
fixture is a Tuple[str, List[Path]]
. The first item is the name of the scenario. The second item is a list of MODFLOW 6 namefile Path
s, ordered alphabetically by name, with models generally named as follows:
groundwater flow models begin with
gwf*
transport models begin with
gwt*
This naming permits models to be run in the order provided, with transport models potentially consuming the outputs of flow models. One possible pattern is to loop over models and run each in a subdirectory of the same top-level working directory.
def test_example_scenario(tmp_path, example_scenario):
name, namefiles = example_scenario
for namefile in namefiles:
model_ws = tmp_path / namefile.parent.name
model_ws.mkdir()
# load and run model
# ...
Note: example models must first be built by running pytest -v -n auto test_scripts.py --init
in modflow6-examples/autotest
before running tests using the example_scenario
fixture. See the install docs for more info.
Filtering
External model test cases can be filtered by model name or by the packages the model uses with the --model
and --package
command line arguments, respectively.
Filtering by model name
Filtering models by name is functionally equivalent to filtering pytest
cases with -k
. (In the former case the filter is applied before test collection, while the latter collects tests as usual and then applies the filter.)
For instance, running the test_largetestmodels.py
script in the MODFLOW-USGS/modflow6
repository’s autotest/
folder, and selecting a particular model from the MODFLOW-USGS/largetestmodels
repository by name:
autotest % pytest -v test_largetestmodels.py --collect-only --model test1002_biscqtg_disv_gnc_nr_dev
...
collected 1 item
...
Equivalently:
autotest % pytest -v test_largetestmodels.py --collect-only -k test1002_biscqtg_disv_gnc_nr_dev
...
collected 18 items / 17 deselected / 1 selected
...
The --model
option can be used multiple times, e.g. --model <model 1> --model <model 2>
.
Filtering by package
MODFLOW 6 models from external repos can also be filtered by packages used. For instance, to select only large GWT models:
autotest % pytest -v --package gwt
Utility functions
Model-loading fixtures use a set of utility functions to find and enumerate models. These functions can be imported from modflow_devtools.misc
for use in other contexts:
get_model_paths()
get_namefile_paths()
These functions are used internally in a pytest_generate_tests
hook to implement the above model-parametrization fixtures. See fixtures.py
and/or this project’s test suite for usage examples.