Oregon State University
2025-02-03
when to test: always.
where to test: external test suite.
Example: tests
subdirectory inside package.
Perfect is the enemy of good; a basic level of tests is better than nothing. But a rigorous test suite will save you time and potential problems in the long run.
Testing is a core principle of scientific software; it ensures results are trustworthy.
Scientific and engineering software is used for planes, power plants, satellites, and decisionmaking. Thus, correctness of this software is pretty important.
And we all know how easy it is to have mistakes in code without realizing it…
Tests compare expected outputs versus observed outputs for known inputs. They do not inspect the body of the function directly. In fact, the body of a function does not even have to exist for a valid test to be written.
For exactness:
def test_kepler_loc():
p1 = jupiter(two_days_ago)
p2 = jupiter(yesterday)
exp = jupiter(today)
obs = kepler_loc(p1, p2, 1, 1)
assert exp == obs
For approximate exactness:
pytest
finds all testing modules and functions, and runs them.
assert a == b
a == pytest.approx(b)
or np.allclose(a,b)
, for both scalars and arrays/listsapprox()
and allclose
allow you to specify relative and absolute tolerancesTwo or more edge cases combined:
import numpy as np
from pytest import approx
from mod import sinc2d
def test_internal():
exp = (2.0 / np.pi) * (-2.0 / (3.0 * np.pi))
obs = sinc2d(np.pi / 2.0, 3.0 * np.pi / 2.0)
assert exp == approx(obs)
def test_edge_x():
exp = (-2.0 / (3.0 * np.pi))
obs = sinc2d(0.0, 3.0 * np.pi / 2.0)
assert exp == approx(obs)
def test_edge_y():
exp = (2.0 / np.pi)
obs = sinc2d(np.pi / 2.0, 0.0)
assert exp == approx(obs)
def test_corner():
exp = 1.0
obs = sinc2d(0.0, 0.0)
assert exp == approx(obs)
from pytest import approx
from mod import std
def test_std1():
obs = std([0.0, 2.0])
exp = 1.0
assert exp == approx(obs)
def test_std2():
obs = std()
exp = 0.0
assert exp == approx(obs)
def test_std3():
obs = std([0.0, 4.0])
exp = 2.0
assert exp == approx(obs)
def test_std4():
obs = std([1.0, 1.0, 1.0])
exp = 0.0
assert exp == approx(obs)
Meaning: percentage of code for which a test exists, determined by number of line executed during tests.
pytest-cov
Instructions:
pytest-cov
using pip install pytest-cov
pytest -vv --cov./
pytest -vv --cov=./ --cov-report html
Work towards 100% coverage, but don’t obsess over it.
Use coverage to help identiy missing edge/corner cases.
Ensure all changes to your project pass tests through automated test & build process.
Recent paper found that calculating nuclear magnetic resonance chemical shifts on different operating systems lead to different outputs!
Using Python scripts published in Nature Protocols in 2014, cited over 130 times.
Error due to differences in file sorting across platforms…
Could have easily been caught with a CI system that tested on multiple platforms.
GitHub now provides a free CI system called GitHub Actions
name: Python package
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install .
pip install flake8 pytest
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
You can add badges to your README!
Can also add badge for test coverage: https://github.com/marketplace/actions/coverage-badge
CI can also be used to build packages and deploy to PyPI, build documentation, etc.