Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added lessons #1

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/hsf-training/hsf-training-pytest-webpage/main.svg)](https://results.pre-commit.ci/latest/github/hsf-training/hsf-training-pytest-webpage/main)
[![pages-build-deployment](https://github.com/hsf-training/hsf-training-pytest-webpage/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/hsf-training/hsf-training-pytest-webpage/actions/workflows/pages/pages-build-deployment)

# Unit testing with pytest
# Unit Testing with Pytest

> **Note**
> Click [here](https://hsf-training.github.io/hsf-training-pytest-webpage/) for the training website!
Expand Down
2 changes: 1 addition & 1 deletion _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
carpentry: "hsf"

# Overall title for pages.
title: "Unit testing with pytest"
title: "Unit Testing with Pytest"

# Life cycle stage of the lesson
# See this page for more details: https://cdh.carpentries.org/the-lesson-life-cycle.html
Expand Down
65 changes: 65 additions & 0 deletions _episodes/01-Introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
title: Introduction
teaching: 5
exercises: 0
questions:
- "Whats is Pytest?"
- "Why test?"
objectives:
- "Understand the place of testing in a scientific workflow."
- "Understand that testing has many forms."
keypoints:
- "Tests check whether the observed result, from running the code, is what was expected ahead of time."
- "Tests should ideally be written before the code they are testing is written, however some tests must be written after the code is written."
- "Assertions and exceptions are like alarm systems embedded in the software, guarding against exceptional bahavior."
- "Unit tests try to test the smallest pieces of code possible, usually functions and methods."
- "Integration tests make sure that code units work together properly."
- "Regression tests ensure that everything works the same today as it did yesterday."
---

# What is Pytest?

<center><img src="https://realpython.com/cdn-cgi/image/width=1920,format=auto/https://files.realpython.com/media/Intermediate-Advanced-PyTest-Features_Watermarked.43fb169e7121.jpg" alt="Pytest" style="width:576px;height:324px;"><p><a href="https://realpython.com/pytest-python-testing/">Via Real Python</a></p></center>

Testing the code is a crucial task that confirms that the code is working correctly and meets the quality standards for customers and, in this case, for analysis. Automated tests are important to confirm that code is working correctly. This is called **defensive programming** and the most common way to do it is to add alarms and tests into our code so that it checks itself.

At its core, Pytest follows the "convention over configuration" principle, which means it provides sensible defaults and encourages a standardized structure for tests. This allows you to focus on writing test logic rather than dealing with complex setup or boilerplate code.

# Why Test?

“Trying to improve the quality of software by doing more testing is like trying to lose weight by weighting yourself more often.” - Steve McConnell

- Testing won’t correct a buggy code
- Testing will tell you were the bugs are…
- … if (and only if) the test cases cover the scenarios that cause the bugs or occur.

Also, automated tests only test a narrow interpretation of quality software development. They do not help test that your software is useful and help solves a users’ problem.

There are many ways to test software, such as:

- Assertions
- Exceptions
- Unit Tests
- Regression Tests
- Integration Tests

*Exceptions and Assertions*: While writing code, `exceptions` and `assertions`
YarelisAcevedo marked this conversation as resolved.
Show resolved Hide resolved
can be added to sound an alarm as runtime problems come up. These kinds of
tests, are embedded in the software iteself and handle, as their name implies,
exceptional cases rather than the norm.

*Unit Tests*: Unit tests investigate the behavior 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 behavior 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.

## 'pytest' vs 'unittest'?

Python has a built in framework for testing called ``unittest``. It is based on the Java x-unit style framework. Unittest requires developers to create classes derived from the TestCase module and then define the test cases as methods in the class. This means that there is a lot of boilerware code required to execute the tests, in contrast to Pytest, which is more condensed and easier to write.
YarelisAcevedo marked this conversation as resolved.
Show resolved Hide resolved
26 changes: 0 additions & 26 deletions _episodes/01-example.md

This file was deleted.

220 changes: 220 additions & 0 deletions _episodes/02-assertions_exeptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
---
title: Assertions and Exceptions
teaching: 10
exercises: 0
questions:
- "How can we compare observed and expected values?"
- "How do I handle unusual behavior while the code runs?"
objectives:
- "Assertions can halt execution if something unexpected happens."
- "Assertions are the building blocks of tests."
- "Learn when to use exceptions and what exceptions are available"
keypoints:
- "The `assert` keyword is used to set an assertion."
- "Assertions halt execution if the argument is false."
- "Assertions do nothing if the argument is true."
- "Exceptions are effectively specialized runtime tests"
- "Exceptions can be caught and handled with a try-except block"
---

Pytest provides a rich set of built-in assertion statements that you can use to check conditions in your tests. These assertions make it easy to express and verify the expected outcomes of your code. The assert keyword in python has the
following behavior:

```python
>>> assert True == False
YarelisAcevedo marked this conversation as resolved.
Show resolved Hide resolved
```

~~~
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
~~~
{: .output}

```python
>>> assert True == True
```

~~~
~~~
{: .output}

(Nothing happened because it "passed" but mind we are not testing something here really, we are just asserting that True is in fact == True)

Assertions halt code execution instantly if the comparison is false.
It does nothing at all if the comparison is true. These are therefore a very
good tool for guarding the function against foolish (e.g. human) input.

The advantage of assertions is their ease of use. They are rarely more than one
line of code. The disadvantage is that assertions halt execution
indiscriminately and the helpfulness of the resulting error message is usually
quite limited.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can add custom error messages to them: assert False, "this will be displayed"


Another simple example of assertions:

```python
>>> # file name: test_sample.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't add the >>> if we're not explicitly in the python repl

>>> import cmath
>>>
>>> def invariant_mass(energy, momentum):
>>> return cmath.sqrt(energy**2 - momentum**2)
>>>
>>> def test_answer():
>>> assert invariant_mass(3, 7) == 1
```

```bash
pytest
```

~~~
collected 1 item

test_sample.py F [100%]

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

def test_answer():
> assert invariant_mass(3, 7) == 1
E assert 6.324555320336759j == 1
E + where 6.324555320336759j = invariant_mass(3, 7)

test_sample.py:7: AssertionError
=========================== short test summary info ============================
FAILED test_sample.py::test_answer - assert 6.324555320336759j == 1
============================== 1 failed in 0.13s ===============================
~~~
{: .output}


Also, input checking may require decending a rabbit hole of exceptional cases.
What happens when the input provided to the invariant_mass function is a string, rather
than a list of numbers? Or even, if it is a NaN result?

1. Open the terminal
2. Create the following function and name the file ``invariant_mass.py``:

```python
import cmath

def invariant_mass(energy, momentum):
return cmath.sqrt(energy**2 - momentum**2)
```

3. In the function, insert an assertion that checks whether the input is actually a list.
YarelisAcevedo marked this conversation as resolved.
Show resolved Hide resolved

> ## Hint
>
> Hint: Use the [isinstance function](https://docs.python.org/2/library/functions.html#isinstance).
{: .callout}

> ## Input as NaN using numpy
>
> What happens if we use numpy instead of cmath in the code? Why we get a `NaN` as an answer?
>
> > ## Solution
> >
> > In `numpy`, NaN is a float and assigning a NaN sets the imaginary part to zero. From the programmer's point
> > of view, it is a logical behavior but not mathematically (definition of not a number). This can be a
> > used as a marker for invalid data, but in this case it doesn't help.
> {: .solution}
{: .challenge}

> ## Testing Near Equality
>
> Assertions are also helpful for catching abnormal behaviors, such as those
> that arise with floating point arithmetic. Using the assert keyword, how could
> you test whether some value is almost the same as another value?
>
> - Use the `assert` keyword to check whether the number a is greater than 2.
> - Use the `assert` keyword to check that a is equal to 2 within an error of 0.003.
{: .callout}

```python
from mynum import a
# greater than 2 assertion here
# 0.003 assertion here
```

## Comparing Numbers within a Tolerance

Pytest as well as numpy has a built-in class for floating-point comparisons called ``pytest.approx``.

```python
>>> from pytest import approx
>>> 0.003 + 2 == approx(2.003)
True
```

# Exceptions

Exceptions are more sophisticated than assertions. They are the standard error
YarelisAcevedo marked this conversation as resolved.
Show resolved Hide resolved
messaging system (unforseen circumstance) in most modern programming languages. Fundamentally, when an
error is encountered, an informative exception is 'thrown' or 'raised'. Mind that a failed assert,
also raises an exception `AssertionError`, so be very careful.

For example, instead of the assertion in the case before, an exception can be
used.

```python
import cmath

def invariant_mass(energy, momentum):
if (energy**2 < momentum**2).any():
raise Exception("Energy has to be greater than momentum")

return cmath.sqrt(energy**2 - momentum**2)
```

Once an exception is raised, it will be passed upward in the program scope.
An exception be used to trigger additional error messages or an alternative
behavior rather than immediately halting code
execution, the exception can be 'caught' upstream with a try-except block.
When wrapped in a try-except block, the exception can be intercepted before it reaches
global scope and halts execution.

To add information or replace the message before it is passed upstream, the try-catch
block can be used to catch-and-reraise the exception:

```python
import cmath

def invariant_mass(energy, momentum):
try:
if (energy**2 < momentum**2).any():
raise ValueError("Energy has to be greater than momentum")

except TypeError:
raise TypeError("Please insert a number or list")
```

If an alternative behavior is preferred, the exception can be disregarded and a
responsive behavior can be implemented like so:

```python
import cmath

def invariant_mass(energy, momentum):
try:
if (energy**2 < momentum**2).any():
raise ValueError("Energy has to be greater than momentum")

elif (energy < 0).any() or (momentum < 0).any():
raise ValueError("Energy and momentum must be positive")

else:
return cmath.sqrt(energy**2 - momentum**2)

except TypeError:
raise TypeError("Please insert a number or list")
```

> ## What else could go wrong?
>
> 1. Think of some other type of exception that could be raised by the try
> block.
> 2. Guard against it by adding an except clause.
> 3. Use the mean function in three different ways, so that you cause each
> exceptional case.
{: .callout}
Loading