Running your code with a set of input parameters, where you already know what the output should be - and checking that your code output and “model answers” match
You probably already informally do this!
A more structured testing suite… - Ensures code doesn’t slip through the cracks - Makes sure that the code still works every time it is updated - Can be integrated with other tools (such as GitHub)
If you’re not testing your code, you’re not following the scientific method
This test will pass (nothing will happen) if the test output matched the expected example output, and will fail and throw an error if there’s a difference
When will == cause problems?
If your test output and your expected example output are not both integers or identical strings!
With scientific code, it’s very likely that the output will be… - Floating point numbers - Arrays of floating point numbers
This is when functions such as pytest.approx() (and equivalent functions in numpy and other libraries which allow comparison of arrays), which allow you to build in a tolerance to your tests, are used.
It’s likely you’ll rarely use the pure equal-to operator.
Testing your computational research…
Testing the code…
Unit Tests: test each function/each small atomic section of your code for a range of different input parameters
Integration Tests: test how the different functions/parts fit and work together
Ensure that the code is doing what you think it’s doing:
Catch bugs quickly
Catch silent errors, where the code still runs but outputs different results
Testing the science…
Are the results sensible? Do they make physical sense?
How to the results compare to evidence/data/analytical solutions/other models?
What assumptions have been made?
Accuracy, precision, stability of numerical models
Test Driven Development
Build the test first!
Create a test for a planned function that will just fail by default
Include expected outputs for comparison against the non-existent test outputs
Start with the test!
First, build your test - imagine the Crank-Nicolson solver function doesn’t exist yet!
If you run the test function, it will fail, with a different error this time: the function now exists, but it has no output.
Test Driven Development
Build the test first!
Create a test for a planned function that will just fail by default
Include expected outputs for comparison against the non-existent test outputs
Put together the scaffolding of the function
Have it take the correct input but not do anything
Continue to build the function until you have a passing test
Running the tests automatically
If you call all of your test functions test_something(), and if you put all your tests in a file called test_something.py, and if you put any test scripts/files in a folder called tests in the main project folder…
You can create/use a conda environment with pytest installed, and simply run:
pytest
from the terminal; it will collect and run any test functions in the tests folder.
Test workflow
Test suites using pytest can be incorporated into your repository.
You can create a GitHub action that will:
Run all of your tests when changes are made to a specific branch
Stop merging of other branches with main if they do not pass the tests
Allow collaborators to quickly run the tests on their new contributed code to save you the hassle of testing it manually.
Putting this into action
Testing can feel complex and scary until you try implementing it!