
(4.4-unit-testing)=
# 🧩 4.4 Unit Testing

```{contents}
:depth: 3
```

## 🔰 Tutorial

Unit testing is a crucial skill for software developers, helping ensure code quality and reliability. This module introduces you to unit testing in Python using the pytest framework, and explores the concept of test-driven development (TDD).

In this module, you will learn the fundamentals of unit testing in Python using the **pytest** framework. You will also explore test-driven development (TDD) and how it can be applied to real-world projects.

1. Explain the purpose of unit tests
2. Write unit tests for the light-mixing demo
3. Run and interpret unit tests to fix code
4. Explain test-driven development (TDD)

### Purpose of Unit Tests

Unit tests are small, isolated tests that validate the functionality of a specific section (unit) of your code, such as a function or a class. They help ensure that individual components of your program behave as expected, making debugging easier and reducing the chances of introducing bugs when making changes.

#### Key Benefits:
- Validate the correctness of your code.
- Catch issues early in the development cycle.
- Provide a safety net for refactoring.
- Improve code quality and maintainability.

**Example:**
```python
def add(a, b):
    return a + b
```

A unit test for this function would check whether it correctly adds two numbers:
```python
def test_add():
    assert add(2, 3) == 5
```

**Video Tutorial**: [What is Unit Testing?](https://www.youtube.com/watch?v=1Lfv5tUGsn8)

### Writing Unit Tests with pytest

**pytest** is a popular Python testing framework that simplifies the process of writing and running tests. It automatically discovers test files and functions, and provides a clean, easy-to-use syntax.

#### Steps:
1. Install **pytest** using pip:
   ```bash
   pip install pytest
   ```
2. Create a test file (e.g., `test_light_mixing.py`) and define your unit tests inside it.
3. Use the `assert` statement to check whether the function outputs match the expected results.

#### Example Unit Test for the Light-Mixing Demo:
```python
# light_mixing.py
def mix_colors(color1, color2):
    if color1 == "red" and color2 == "blue":
        return "purple"
    elif color1 == "blue" and color2 == "yellow":
        return "green"
    elif color1 == "red" and color2 == "yellow":
        return "orange"
    else:
        return "unknown"


# test_light_mixing.py
def test_mix_colors():
    assert mix_colors("red", "blue") == "purple"
    assert mix_colors("blue", "yellow") == "green"
    assert mix_colors("red", "yellow") == "orange"
```

#### Running Tests:
1. Navigate to the folder containing your test file.
2. Run pytest from the command line:
   ```bash
   pytest
   ```

pytest will automatically discover all files starting with `test_` and execute the tests.

#### Naming Conventions:
- Test files should be named `test_*.py` or `*_test.py`
- Test functions should start with `test_`
- Test classes should start with `Test`

These naming conventions help pytest automatically discover your tests.

**Video Tutorial**: [Introduction to pytest](https://www.youtube.com/watch?v=Kt6QqGoAlvI&ab_channel=teclado)

### Interpreting Test Results

When you run pytest, it will display the results of your tests in the terminal. Here's how to interpret the output:

- **Green (PASSED)**: The test passed successfully.
- **Red (FAILED)**: The test failed. pytest will show you the expected result and the actual result so you can identify the issue.
- **Yellow (SKIPPED)**: The test was skipped (typically because of a specific condition, such as platform dependency).

#### Example Output:
```bash
============================= test session starts ==============================
collected 3 items

test_light_mixing.py ...                                                [100%]

============================== 3 passed in 0.03s ===============================
```

If a test fails, pytest will provide a detailed report:
```bash
def test_mix_colors():
>       assert mix_colors("blue", "yellow") == "purple"
E       AssertionError: assert 'green' == 'purple'

test_light_mixing.py:5: AssertionError
```

In this case, you can see that the test expected "purple" but received "green," indicating an issue with the `mix_colors()` function.

#### Code Coverage:
pytest can be used with the `pytest-cov` plugin to measure code coverage, which indicates how much of your code is being tested:

```bash
pip install pytest-cov
pytest --cov=myproject tests/
```

This will show you the percentage of your code that is covered by tests, helping you identify areas that need more testing.

**Video Tutorial**: [Understanding pytest Output](https://www.youtube.com/watch?v=dN-pVt7i4Us&ab_channel=anthonywritescode)

### Debugging with pytest

When a test fails, you can use pytest's built-in debugging capabilities to help identify the issue. The `--pdb` flag can be used to drop into the Python debugger when a test fails, allowing you to inspect variables and step through the code.

#### Steps:
1. Run pytest with the `--pdb` flag:
   ```bash
   pytest --pdb
   ```
2. When a test fails, pytest will drop into an interactive debugging session where you can inspect variables and explore the state of your program.

#### Example:
```bash
(Pdb) print(color1)
'blue'
(Pdb) print(color2)
'yellow'
```

**Video Tutorial**: [Debugging with pytest](https://www.youtube.com/watch?v=by9ZU7h1cgk&ab_channel=SuperEngineer)

### Test-Driven Development (TDD)

**Test-driven development (TDD)** is a software development approach where you write tests before writing the actual code. This ensures that the code you write is directly influenced by the tests, leading to a more robust and bug-free implementation.

#### TDD Workflow:
1. **Write a Test**: Begin by writing a test for the new functionality you want to implement.
2. **Run the Test**: Since the functionality doesn't exist yet, the test will fail.
3. **Write Code**: Write just enough code to make the test pass.
4. **Run the Tests Again**: Ensure all tests pass. If any tests fail, fix the code until they pass.
5. **Refactor**: Clean up the code while ensuring that the tests continue to pass.

#### Example of TDD for Light Mixing:
1. **Write the Test**:
   ```python
   def test_mix_colors():
       assert mix_colors("red", "blue") == "purple"
   ```
2. **Run the Test**: It will fail because the `mix_colors()` function doesn’t exist yet.
3. **Write the Function**:
   ```python
   def mix_colors(color1, color2):
       if color1 == "red" and color2 == "blue":
           return "purple"
       return "unknown"
   ```
4. **Run the Test Again**: The test should now pass.
5. **Refactor the Code**: Improve the `mix_colors()` function while ensuring the test still passes.

**Video Tutorial**: [Test-Driven Development with pytest](https://www.youtube.com/watch?v=uEFrE6cgVNY)

### Additional Resources

- [pytest Documentation](https://docs.pytest.org/en/6.2.x/)
- [Unit Testing in Python](https://realpython.com/python-testing/)
- [Test-Driven Development Explained](https://martinfowler.com/bliki/TestDrivenDevelopment.html)
- [Python Testing with pytest: Simple, Rapid, Effective, and Scalable](https://pragprog.com/titles/bopytest/python-testing-with-pytest/) (book)
- [Effective Python Testing With Pytest](https://realpython.com/pytest-python-testing/) (in-depth tutorial)
- [Property-Based Testing With Hypothesis and Pytest](https://semaphoreci.com/community/tutorials/property-based-testing-with-hypothesis-and-pytest)

## 🚀 Quiz

::::{tab-set}
:sync-group: category

:::{tab-item} W 2024
:sync: w2024

:::

:::{tab-item} Sp/Su 2024
:sync: sp2024

https://q.utoronto.ca/courses/370070/assignments/1393625?display=full_width
:::

:::{tab-item} Sp/Su 2025
:sync: sp2025

https://q.utoronto.ca/courses/393352/assignments/1499727?display=full_width_with_nav
:::

::::

## 📄 Assignment

::::{tab-set}
:sync-group: category

:::{tab-item} W 2024
:sync: w2024

:::

:::{tab-item} Sp/Su 2024
:sync: sp2024

https://q.utoronto.ca/courses/370070/assignments/1394230?display=full_width
:::

:::{tab-item} Sp/Su 2025
:sync: sp2025

https://q.utoronto.ca/courses/393352/assignments/1499728?display=full_width_with_nav
:::

::::
