10. Unit Testing#
This chapter was coauthored by Jason DeBacker and Richard W. Evans.
As a code base expands and the scripts and modules become more interdependent and interconnected, the probability increases that additions to the code will introduce bugs. And as the code base becomes bigger, the harder it can be to find bugs. One of the primary ways to protect the functionality of a code base from bugs is unit testing.
10.1. PyTest#
Testing of your source code is important to ensure that the results of your code are accurate and to cut down on debugging time. Fortunately, Python
has a nice suite of tools for unit testing. In this section, we will introduce the pytest
package and show how to use it to test your code.
The iframe below contains a PDF of the BYU ACME open-access lab entitled, “Unit Testing”. You can either scroll through the lab on this page using the iframe window, or you can download the PDF for use on your computer. See [BYU ACME, 2023]. Exercise 10.1 below has you work through the problems in this BYU ACME lab. Two Python scripts (specs.py
and test_specs.py
) used in the lab are stored in the ./code/UnitTest/
directory.
10.2. Code coverage#
Ideally, one wants to make sure that all of their source code is tested, thereby ensuring it is producing expected results and reducing the potential that new contributions will introduce bugs. But for any significant code base, it is difficult to know which lines of code are tested and which are. To get an understanding of what is covered by unit tests, packages like coverage.py
can be used to automatically generate a report of code coverage. The report will show which lines of code are covered by unit tests and which are not. This can be useful for identifying parts of the code that need more testing.
10.3. Continuous integration testing and GitHub Actions#
When using GitHub to collaborate with others on a code base, one can leverage the ability to use GitHub Actions to automate unit testing and code coverage reports (as well as other checks on might want to run). GitHub actions are specified in yaml files and triggered by some set event (e.g., a push, or a pull request, or a chronological schedule). One of the most effective ways to ensure new contributions are not introducing bugs is to run unit tests and code coverage reports on every push to the repository. This can be done by creating a GitHub action that runs the unit tests and code coverage report on every push to the repository. Codecov provides some useful tools for reporting code coverage from unit tests in GitHub Actions. You can see the actions OG-Core
uses here. These include unit tests and coverage reports, as well as checks that documentation builds and then is published upon a merge to the master
branch.
10.4. Exercises#
Read the BYU ACME “Unit Testing” lab and complete Problems 1 through 6 in the lab. [BYU ACME, 2023]
In Chapter SciPy: Root finding, minimizing, interpolation, Exercise 8.1, you wrote wrote a function, and called SciPy.optimize
to minimize that function. This function had an analytical solution so you could check that SciPy obtained the correct constrained minimum. Now, write a test_min
function in a module named test_exercises.py
. This function should end with an assert statement that the minimum value of the function is equal to the analytical solution. Then, run the test using pytest
and make sure it passes. Note, if your wrote the original function for Exercise 8.1 in a notebook, copy it over to a module can save it as exercises.py
.
Write another test in your test_exercises.py
module that uses an assert statement to test that the type of the output of your test_min
function is a NumPy ndarray
object. Then, run the test using pytest
and make sure it passes.
Write a simple function that returns the sum of two digits:
def my_sum(a, b):
return a + b
Save this in a module called exercises.py
. Now, use the @pytest.mark.parametrize
decorator to test a function for multiple inputs of a
and b
.
Use the @pytest.mark
decorator to mark one of your tests in test_exercises.py
. Then, your tests using pytest
but in a way that skips tests with the marker you just gave.