Setting Up the Tests
This page explains the testing framework and explains how to run the tests.
Testing Framework
This project uses pytest
to collect and run tests. These tests can
be used to validate that your virtual environment is set up correctly. pytest
provides useful features that make it simpler to write tests. Some basic
features are covered below.
Writing Tests
The following snippet highlights useful principles to follow when writing tests
from pathlib import Path
def test_should_create_existing_temporary_directory(tmp_path: Path) -> None:
assert tmp_path.exists()
Some notes:
- This test check tests that the temporary directory provided exists. If the
directory does not exist, the assertion will raise an error and
pytest
will report that the test fails. - This test requests the
tmp_path
fixture. - Each test should be contained in a function.
- Tests should be relatively simple (this is the idea of "unit-testing").
- Test functions should have descriptive names of the form
test_should_do_something
(wheredo_something
is replaced with the expected behaviour of the test). It is okay if the name is long or verbose. Clarity is the main priority. Someone should be able to get an idea of what the function is testing just from reading the name without having to read the code. If you're finding, however, that you're not able to simply describe what a function should test, that may be a sign that it needs to be broken up into smaller tests. - Strictly speaking, the return annotation for tests should be
None
. - The condition for a test should be checked with an
assert
statement.
Using pytest
Fixtures
pytest
Fixtures are functions that return a value or perform an action
and can be "requested" by test functions. By "requested", we mean that a test
function can require that the fixture function is executed and, optionally,
that its output be provided to the test function. For example, a fixture may
create an Atoms
object that is used by a test function to perform a DFT
calculation. Additionally, fixtures may perform tasks like changing the current
working directory, creating files, or requesting other fixtures. pytest
and
its related plugins provide built-in fixtures, or you can
write your own. Fixtures are declared by using the
pytest.fixture
decorator:
import pytest
from ase.build import molecule
@pytest.fixture(name="co2")
fixture_co2() -> Atoms:
co2 = molecule("CO2")
return co2
You can then "request" the fixture in a test function like so:
Tip
If you notice that you are using the same variable in multiple tests, this is a great use-case for fixtures! For example:
from ase.build import fcc111
def test_should_contain_nickel() -> None:
nickel_unit_cell = fcc111(
"Ni", size=[1, 2, 1], a=3.55, orthogonal=True
)
assert "Ni" in nickel_unit_cell.symbols
def test_should_have_positive_lattice_constant() -> None:
nickel_unit_cell = fcc111(
"Ni", size=[1, 2, 1], a=3.55, orthogonal=True
)
assert (nickel_unit_cell.cell[0] @ nickel_unit_cell.cell[0])**(1/2) > 0
nickel_unit_cell
fixture and perform
separate checks on the returned Atoms
object. But the same code can be
simplified as follows:
from ase.build import fcc111
@pytest.fixture(name="nickel_unit_cell")
fixture_nickel_unit_cell() -> Atoms:
nickel_unit_cell = fcc111(
"Ni", size=[1, 2, 1], a=3.55, orthogonal=True
)
return nickel_unit_cell
def test_should_contain_nickel(nickel_unit_cell: Atoms) -> None:
assert "Ni" in nickel_unit_cell.symbols
def test_should_have_positive_lattice_constant(nickel_unit_cell: Atoms) -> None:
assert (nickel_unit_cell.cell[0] @ nickel_unit_cell.cell[0])**(1/2) > 0
Now, it is more clear what each individual test is testing, and scaling the test suite to include more test cases doesn't require repeating the same "boilerplate" code.
Skipping Tests
Tests can be skipped conditionally using pytest.mark.skipif
.
import sys
import pytest
@pytest.mark.skipif(
sys.version_info < (3,11),
reason="requires Python 3.11 or higher"
)
def test_should_be_skipped_if_python_version_less_than_311() -> None:
pass
or at the command line by (de-)selecting tests using markers.
The above snippet deselects all tests marked with the "calculator" marker.
Running Tests
-
To set up the test framework, ensure that the optional "test" dependencies have been installed. Run the following from the root directory of the welcome guide to install the required dependencies into the current environment.
-
Call
pytest
. -
Verify that all tests pass.
====================================== test session starts ====================================== platform darwin -- Python 3.11.9, pytest-8.2.2, pluggy-1.5.0 -- ~/welcome-guide/bin/python cachedir: .pytest_cache rootdir: ~/welcome-guide configfile: pyproject.toml plugins: datadir-1.5.0, cov-5.0.0, xdist-3.6.1 8 workers [5 items] scheduling tests via LoadScheduling tests/test_gpaw.py::test_gpaw_calculator tests/test_atom_manipulations.py::test_should_create_slab_with_water_layer tests/test_gpaw.py::test_should_perform_gpaw_calculation tests/test_vasp.py::test_vasp_calculator [gw3] [ 20%] PASSED tests/test_gpaw.py::test_should_perform_gpaw_calculation [gw4] [ 40%] PASSED tests/test_vasp.py::test_vasp_calculator [gw1] [ 60%] PASSED tests/test_atom_manipulations.py::test_should_create_slab_with_water_layer tests/test_atom_manipulations.py::test_should_create_nickel_slab [gw0] [ 80%] PASSED tests/test_atom_manipulations.py::test_should_create_nickel_slab [gw2] [100%] PASSED tests/test_gpaw.py::test_gpaw_calculator ============================================ PASSES ============================================= ==================================== short test summary info ==================================== PASSED tests/test_atom_manipulations.py::test_should_create_slab_with_water_layer PASSED tests/test_atom_manipulations.py::test_should_create_nickel_slab PASSED tests/test_gpaw.py::test_should_perform_gpaw_calculation PASSED tests/test_vasp.py::test_vasp_calculator PASSED tests/test_gpaw.py::test_gpaw_calculator ====================================== 5 passed in 351.31s ======================================
Warning
Some tests will be skipped if the required dependencies are not installed. Others will fail if your environment is not set up for the ASE calculator interface.
==================================================== test session starts ===================================================== platform darwin -- Python 3.11.9, pytest-8.2.2, pluggy-1.5.0 -- ~/welcome-guide/bin/python cachedir: .pytest_cache rootdir: ~/welcome-guide configfile: pyproject.toml plugins: datadir-1.5.0, cov-5.0.0, xdist-3.6.1 8 workers [5 items] scheduling tests via LoadScheduling tests/test_atom_manipulations.py::test_should_create_slab_with_water_layer tests/test_vasp.py::test_vasp_calculator [gw4] [ 20%] SKIPPED tests/test_vasp.py::test_vasp_calculator tests/test_gpaw.py::test_gpaw_calculator [gw2] [ 40%] SKIPPED tests/test_gpaw.py::test_gpaw_calculator tests/test_gpaw.py::test_should_perform_gpaw_calculation [gw3] [ 60%] SKIPPED tests/test_gpaw.py::test_should_perform_gpaw_calculation tests/test_atom_manipulations.py::test_should_create_nickel_slab [gw0] [ 80%] PASSED tests/test_atom_manipulations.py::test_should_create_nickel_slab [gw1] [100%] PASSED tests/test_atom_manipulations.py::test_should_create_slab_with_water_layer =========================================================== PASSES =========================================================== ================================================== short test summary info =================================================== PASSED tests/test_atom_manipulations.py::test_should_create_nickel_slab PASSED tests/test_atom_manipulations.py::test_should_create_slab_with_water_layer SKIPPED [1] tests/test_vasp.py:14: The environment is not configured for VASP. See the ASE documentation for details. SKIPPED [1] tests/test_gpaw.py:13: GPAW is not installed. SKIPPED [1] tests/test_gpaw.py:32: GPAW is not installed. ================================================ 2 passed, 3 skipped in 2.67s ================================================
Follow the instructions in the "short test summary info" to resolve the issue.