Testing

Objectives

Duration: 60 minutes

Testing is the only way to keep your code base maintained on multiple versions of Python, ensure its accuracy against known cases, and prevent regressions. In this lesson, we’ll go over the basics to testing, setup a test suit on our project, run it locally, and then run it on TravisCI. Finally, we’ll check the test coverage on our project.

Add (canned) library code to repository

For us to write tests, we’ll need come code to test. To save time, we’ve provided code for a simple library in the tutorial’s repository.

The hugs library (hugely useful geoscience stuff) is a demo library that we’ve created for this tutorial. It contains some geology/geophysics and atmospheric science equations and plots that we can write tests for.

Activate and install hugs

Writing tests with PyTest

PyTest is one of the most commonly used Python testing frameworks. We are going to cover the basic concept of test writing and explore the test coverage of the hugs library.

There are many types of software testing and schools of thought on it, but here we’ll define a few terms and how we’ll be using them in the context of this tutorial.

Unit Testing - Testing a single unit of code (function, method, etc) for the correct or expected outputs.

Integration Testing - Testing that multiple segments of code can function together as expected.

Regression Testing - Testing that previously developed functionality or bug fixes still function as expected.

Another buzz-word is test-driven-development (TDD). This is the process of writing the test for the expected behavior and API before development of the code. For example, writing the test for the correct output of a function before fixing a reported bug.



Pytest looks for files named test*.py and will run the functions named test* in those files.

Creating Tests

Activity

Solution

  def test_wind_comps_basic():
      """Test the basic wind component calculation."""
      speed = np.array([4, 4, 4, 4, 25, 25, 25, 25, 10.])
      dirs = np.array([0, 45, 90, 135, 180, 225, 270, 315, 360])
      s2 = np.sqrt(2.)

      u, v = get_wind_components(speed, dirs)

      true_u = np.array([0, -4 / s2, -4, -4 / s2, 0, 25 / s2, 25, 25 / s2, 0])
      true_v = np.array([-4, -4 / s2, 0, 4 / s2, 25, 25 / s2, 0, -25 / s2, -10])

      assert_array_almost_equal(true_u, u, 4)
      assert_array_almost_equal(true_v, v, 4)


  def test_wind_comps_scalar():
      """Test wind components calculation with scalars."""
      u, v = get_wind_components(8, 150)
      assert_almost_equal(u, -4, 3)
      assert_almost_equal(v, 6.9282, 3)

Other Common Tests

Test that a warning is raised

Some functions will raise warnings and we want to ensure that indeed that is happening. We can use pytest.warns to check this. It’s used as a context manager.

Activity

Solution

def test_warning_direction():
    """Test that warning is raised wind direction > 360."""
    with pytest.warns(UserWarning):
        get_wind_components(3, 480)

Test that an exception is raised

In cases where an exception is raised, we want to check that it indeed is as well. We do this similarly to the warning check using pytest.raises as a context manager.

Activity Add a test to ensure that a ValueError is raised on the snell_angle function when the top layer velocity is zero.

Solution

def test_snell_zero_velocity_top():
    """That that a value error is raised with a zero velocity top layer."""
    with pytest.raises(ValueError):
        snell_angle(12, 0, 4000)

Coverage with coverage.py and pytest-cov

We also want to know what the test coverage of our library is - have we missed anything? We can use coverage.py to check this and pytest-cov lets us do run this tool on all of our test suite and see the total library coverage.