Testing
Confidence that your code works correctly
In this section, we will learn about the importance of testing, how to create a test suite for our code, and how to implement test-driven-development.
Open introduction presentation ↗
Presentation content
What do we mean by testing?
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
How do tests work? Example: Python
Example function:
def CrankNicolsonSolver(arg1, arg2, arg3, arg4):
“““Your Docstring Goes Here”””complex math goes here
amazingly return(output1, output2, output3)
How do we test this?
assert
statements!
In general, Python tests rely on assert statements, comparing example data outputs with the return values of one of your functions.
Essentially:
def test_CrankNicolsonSolver():
= [ex_in1, ex_in2, ex_in3, ex_in4]
example_input = [ex_out1, ex_out2, ex_out3]
example_output = CrankNicolsonSolver(example_input)
test_output assert test_output == example_output
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!
Essentially:
def test_CrankNicolsonSolver():
= [ex_in1, ex_in2, ex_in3, ex_in4]
example_input = [ex_out1, ex_out2, ex_out3]
example_output = CrankNicolsonSolver(example_input)
test_output assert test_output == example_output
This test will fail because an error will be thrown because test_CrankNicolsonSolver()
doesn’t exist yet!
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
Start to build the function
Example function scaffolding:
def CrankNicolsonSolver(arg1, arg2, arg3, arg4):
“““Your Docstring Goes Here”””pass
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!
Start testing your code in the next practical.