Software development

Good practices and tips

Instructors

  • Patricia Ternes - p.ternesdallagnollo@leeds.ac.uk

Part 3.2: Testing & Debugging

Training Material:

Environment: Conda example

Create environment from a yml file

The environment.yml file specifies the dependencies that will be installed in your environment.:

name: env-notebook
dependencies:
  - python>=3.9
  - ipykernel
$ conda env create -f environment1.yml
$ conda activate env-notebook

Conda: Managing packages

$ conda search package-name
$ conda install package-name
$ conda update package-name
$ conda remove package-name
$ conda list

Conda: Managing environments

$ conda env list
$ conda remove -n ENVNAME --all
$ conda env export --name ENVNAME > file-name.yml
$ conda env export --name ENVNAME --no-builds | grep -v "prefix" > file-name.yml
$ conda create --clone ENVNAME --name NEWENV

Check this Conda Cheat Sheet!

Assertions

When Python sees one, it evaluates the assertion’s condition. If it’s true, Python does nothing, but if it’s false, Python halts the program immediately and prints the error message if one is provided.

Assertion Syntax

assert condition, Optional Error Message
# AssertionError with error_message.
a = 1
b = 0
assert b != 0, "Invalid Operation" # denominator can't be 0
print(a / b)
# AssertionError without error_message.
a = 1
b = 0
assert b != 0  # denominator can't be 0
print(a / b)

Assertion

Example

def average(values):
    return (sum(values)/len(values))

What pre-conditions and post-conditions we should write?

Assertion

Example

def average(values):
    return (sum(values)/len(values))

What pre-conditions and post-conditions we should write?

# check if it is a list
assert isinstance(values, list)

# check if all values are integer or real numbers
assert all([isinstance(item, int) or isinstance(item, float) for item in values]

# check list length. Should be greater than zero values
assert len(values)>0

# Average is within the range of the given data
assert average([2,20])>2 and average([2,20])<20
assert average(values)>min(values) and average(values)<max(values)

# Test if works with equal values
assert average([2, 2, 5])==3

# Test if works with negative values
assert average([-2, 2, 6])==2

Assertion

Example

def get_total_cars(values):
    values = [int(element) for element in values]
    total = sum(values)
    return total

Consider the following function

Given a sequence of a number of cars, the function get_total_cars returns the total number of cars. So, get_total_cars([1, 2, 3, 4]) should return 10.

Assertion

Example

def get_total(values):
    assert len(values) > 0
    for element in values:
        assert int(element)
    values = [int(element) for element in values]
    total = sum(values)
    assert total > 0
    return total

What the assertions in this function check

A few reasons to do testing

  • testing saves time.

  • tests (should) ensure code is correct

  • runnable specification: best way to let others know what a function should do and not do.

  • reproducible debugging: debugging that happened and is saved for later reuse

  • easier to modify since results can be tested

Tests at different scales

  • Unit Tests: Unit tests investigate the behaviors of units of code (such as functions, classes, or data structures). By validating each software unit across the valid range of its input and output parameters, tracking down unexpected behaviors that may appear when the units are combined is made vastly simpler.

  • Regression Tests: Regression tests defend against new bugs, or regressions, which might appear due to new software and updates.

  • Integration Tests: Integration tests check that various pieces of the software work together as expected.

Debugging Steps

“My program doesn’t work” isn’t good enough: in order to diagnose and fix problems, we need to be able to tell correct output from incorrect.

1. Know what it’s supposed to do

Debugging Steps

  • We can only debug something when it fails, so the second step is always to find a test case that makes it fail every time.

  • The “every time” part is important because few things are more frustrating than debugging an intermittent problem.

2. Make it fail every time

Debugging Steps

  • If it takes 20 minutes for the bug to surface, we can only do three experiments an hour.

3. Make it fail fast

# AssertionError with error_message.
a = 1
b = 0
assert b != 0, "Invalid Operation" # denominator can't be 0
print(a / b)

Debugging Steps

  • Every time we make a change, however small, we should re-run our tests immediately.

  • The more things we change at once, the harder it is to know what’s responsible for what.

4. Change one thing at a time

Debugging Steps

  • Good scientists keep track of what they’ve done so that they can reproduce their work.

  • Don’t waste time repeating the same experiments or running ones whose results won’t be interesting.

5. Keep track of what you’ve done

Debugging Steps

  • If we can’t find a bug in 10 minutes, we should be humble and ask for help.

  • Asking for help also helps alleviate confirmation bias.
  • People who aren’t emotionally invested in the code can be more objective, which is why they’re often able to spot the simple mistakes we have overlooked.

6. Be humble

Testing framework: pytest

Each time we make changes to a code, we would like to test it.

 

This can be tedious and that might prevent us from testing.

 

We want to make testing as easy as version control is. A testing framework can help us.

 

In pytest, each test is a function whose name begins with the letters test. We can group tests together in files whose names also begin with the letters test. To execute our tests we run the command pytest.

Test & pytest: simple example

# example method
def inc(x):
    return x + 1
# content of test_sample.py
def test_answer():
    assert inc(3) == 5
$ pytest
============================= test session starts =============================
collected 1 items

test_sample.py F

================================== FAILURES ===================================
_________________________________ test_answer _________________________________

    def test_answer():
>       assert inc(3) == 5
E       assert 4 == 5
E        +  where 4 = inc(3)

test_sample.py:5: AssertionError
========================== 1 failed in 0.04 seconds ===========================

By simple executing pytest:

Consider the following

Test-driven development

For example, suppose we need to find where two or more time series overlap.

The range of each time series is represented as a pair of numbers, which are the time the interval started and ended.

 

The output is the largest range that they all include!

Test-driven development

  • Write a short function for each test.

  • Write a range_overlap function that should pass those tests.

  • If range_overlap produces any wrong answers, fix it and re-run the test functions.

file: overlap.py

def range_overlap():
	# insert here your function
    

file: test_overlap.py

from overlap import range_overlap

def test_something():
	# insert here your test

def test_anotherthing():
	# insert here your test