# Testing --- ## Why Test?
  • Preserve functionality
    • Detect new errors early
    • Facilitate reproducibility for research software
  • Help users
    • Verify correct installation
    • Improve correctness for research output
  • Enable developers
    • Make refactoring easier
    • Simplify external contributions

🧮 Manage Complexity 🧩

--- ## Test Types
  • Unit test
    • Smallest possible unit
    • No dependency on outside code...
    • (... replace them with mocks, stubs, etc.)
  • Integration test
    • Test unit interaction
    • Can be on small scales, or system wide

via Gfycat

--- ## How much testing is enough? Test metrics: - lines of code : lines of tests (target: 1:3) - test coverage [example](https://sonarcloud.io/component_measures?id=eWaterCycle_ewatercycle&metric=coverage&view=treemap&selected=eWaterCycle_ewatercycle%3Aewatercycle) --- # PyTest - recommended python testing framework - [docs.pytest.org](https://docs.pytest.org/en/7.3.x/) ![](.files/pytest_logo.svg) --- ## Write Code

$ mkdir pytest-example
$ cd pytest-example
Creating a file example.py containing

def add(a, b):
    return a + b
 
 
def test_add():  # Special name!
    assert add(2, 3) == 5  # What's `assert`? 🤔
    assert add('space', 'ship') == 'spaceship'
Chat with the python shell about assert ...

>>> assert 1==1  # passes
>>> assert 1==2  # throws error
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
--- ## Test!

$ pytest example.py
======================== test session starts ========================
platform linux -- Python 3.6.9, pytest-7.0.1, pluggy-1.0.0
rootdir: /home/ole/Desktop/pytest-texample
collected 1 item

example.py .                                                  [100%]

========================= 1 passed in 0.00s =========================
--- ## Breaking Things

def add(a, b):
    return a - b  # Uh oh, mistake! 😱


def test_add():
    assert add(2, 3) == 5
    assert add('space', 'ship') == 'spaceship'
--- ## Testing Again

$ pytest example.py
======================== test session starts =========================
platform linux -- Python 3.6.9, pytest-7.0.1, pluggy-1.0.0
rootdir: /home/ole/Desktop/pytest-texample
collected 1 item

example.py F                                                   [100%]

============================== FAILURES ==============================
______________________________ test_add ______________________________

    def test_add():
>       assert add(2, 3) == 5
E       assert -1 == 5
E        +  where -1 = add(2, 3)

example.py:6: AssertionError
====================== short test summary info =======================
FAILED example.py::test_add - assert -1 == 5
========================= 1 failed in 0.05s ==========================
  • 🚀❓Functions fail on first error
  • But all test functions are executed
--- ## Take-away - pytest collects and runs all test functions starting with test_ - The tests pass when they do not throw (assertion) errors

def steal_sheep():
    ...
def paint_cows():
    ...

# optionally in another file:

def test_steal_sheep():
    ...
def test_paint_cows():
    ...
--- # Recap: pure functions
  • Are deterministic
  • Have a return value
  • Have no side effects[1]
  • Have referential transparency[2]

def last(my_array):
    return my_array[-1]
 
def add(a, b):
    return a + b
 
print(add(1, 2))
print(3)

Pure functions are easy to understand and test!

[1] Side effects: interactions of a function with its surroundings
[2] Replacing a function call with the return of that function should not change anything
--- ## Impure Functions
intuitive...

my_list = []
 
def append_to_my_list(item):
    my_list.append(item)
 
 
def read_data(file_name):
    return pd.read_csv(file_name)
 
 
def get_random_number(number):
    return random.random()
 
... and not so intuitive

def hello(name):
    print("Hello", name)
 
 
nums = [1, 2]
 
def append(a_list, item):
    a_list += [item]
    return a_list
 
print(nums)            # [1, 2]
print(append(nums, 3)) # [1, 2, 3]
print(nums)            # [1, 2, 3] 😬
  • Side effects are sometimes necessary
  • Some side effects are hard to spot
--- ## Take-away - Use pure functions when possible 👌 - Testing does not have to be hard 👏 - You test anyways, but then throw the test away 🧐 - You don't have to strive for 💯% test coverage - Aim for a balance between unit- and integration tests ⚖️ - Testing removes the dread of refactoring 🔁 - Your future you will thank you 🙏 --- ## Test-Driven Development: FizzBuzz Function
  • fizz_buzz() takes an integer argument and returns it, BUT
    • fails on zero or negative numbers
    • instead returns "Fizz" on multiples of 3
    • instead returns "Buzz" on multiples of 5
    • instead returns "FizzBuzz" on multiples of 3 and 5
--- ## FizzBuzz Function
  • fizz_buzz() takes an integer argument and returns it, BUT
    • fails on zero or negative numbers
    • instead returns "Fizz" on multiples of 3
    • instead returns "Buzz" on multiples of 5
    • instead returns "FizzBuzz" on multiples of 3 and 5
  • Create an empty function fizz_buzz()
  • Write the tests
  • Paste your tests in the collab document, and discuss
  • Now write a function code to make your tests pass
--- ## Take-away - What did you think of this style of development? - Was it easier or harder than just writing code? - Would your code look different without the tests? - For what kind of projects would it be useful?
Test-Driven Development (TDD) is an optional tool in your toolbox 🛠️