# 104. Unit Testing with pytest

**1. Basic Test with `assert` Statement**

```python
# test_example.py
def test_addition():
    result = 2 + 3
    assert result == 5  # This test will pass
```

**Explanation**: This is a simple test where we check if the addition of 2 and 3 equals 5. The `assert` statement is used to verify the correctness of the output.

***

**2. Testing Functions with Multiple Assertions**

```python
# test_example.py
def test_multiplication_and_division():
    assert 2 * 3 == 6
    assert 9 / 3 == 3
```

**Explanation**: We can use multiple assertions in a single test to check different conditions.

***

**3. Using `pytest.mark.parametrize` to Parametrize Tests**

```python
# test_example.py
import pytest

@pytest.mark.parametrize("a, b, expected", [(2, 3, 5), (5, 5, 10), (1, 9, 10)])
def test_addition(a, b, expected):
    assert a + b == expected
```

**Explanation**: `pytest.mark.parametrize` allows us to run the same test with different input values, reducing code duplication and testing various cases.

***

**4. Testing Exceptions with `pytest.raises`**

```python
# test_example.py
import pytest

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        result = 1 / 0
```

**Explanation**: `pytest.raises` is used to check if an exception is raised when a specific code is executed. In this case, dividing by zero should raise a `ZeroDivisionError`.

***

**5. Using Fixtures for Test Setup**

```python
# test_example.py
import pytest

@pytest.fixture
def setup_data():
    return {"name": "Alice", "age": 30}

def test_greeting(setup_data):
    assert setup_data["name"] == "Alice"
    assert setup_data["age"] == 30
```

**Explanation**: `pytest.fixture` allows us to define setup code that can be shared across multiple tests. Here, `setup_data` is used to provide data for the `test_greeting` function.

***

**6. Test Skipping with `pytest.mark.skip`**

```python
# test_example.py
import pytest

@pytest.mark.skip(reason="This test is skipped for now")
def test_addition():
    assert 2 + 3 == 5
```

**Explanation**: `pytest.mark.skip` is used to skip a test. You can also add a reason for skipping the test.

***

**7. Test Condition with `pytest.mark.skipif`**

```python
# test_example.py
import pytest
import sys

@pytest.mark.skipif(sys.version_info < (3, 7), reason="Python version too old")
def test_feature_that_requires_python37():
    assert True
```

**Explanation**: `pytest.mark.skipif` is used to skip tests based on certain conditions. In this example, the test is skipped if the Python version is less than 3.7.

***

**8. Testing a Class Method**

```python
# test_example.py
class Calculator:
    def add(self, a, b):
        return a + b
    
def test_add_method():
    calc = Calculator()
    result = calc.add(2, 3)
    assert result == 5
```

**Explanation**: Here, we test a method of a class (`add`) to ensure it performs the correct operation.

***

**9. Using `pytest.fixture` for Cleanup (Teardown)**

```python
# test_example.py
import pytest

@pytest.fixture
def setup_and_teardown():
    print("\nSetting up resources")
    yield "some_data"
    print("\nTearing down resources")

def test_resource(setup_and_teardown):
    assert setup_and_teardown == "some_data"
```

**Explanation**: Fixtures can also handle teardown. In the above example, `yield` allows us to perform cleanup after the test is executed.

***

**10. Test Coverage with `pytest-cov`**

```bash
# To run tests with coverage, use pytest with the `--cov` flag:
pytest --cov=your_module_name
```

**Explanation**: `pytest-cov` is a plugin that helps you measure test coverage. You can use the `--cov` flag to specify the module for which you want to check the test coverage.

***

**Running the Tests:**

1. **To run the tests**:

   * Save the above code snippets into a file named `test_example.py`.
   * Run the tests with `pytest` in the terminal:

   ```bash
   pytest test_example.py
   ```
2. **To install `pytest`**:

   * Install it via pip:

   ```bash
   pip install pytest
   ```

***

**Summary of Key Concepts:**

* **`assert`**: Used for basic assertions in tests.
* **`pytest.mark.parametrize`**: Parametrizes the test to run with multiple sets of inputs.
* **`pytest.raises`**: Used to test for exceptions in a block of code.
* **`pytest.fixture`**: Provides setup and teardown functionality for tests.
* **Test skipping**: Skipping tests using `skip` or conditionally with `skipif`.
* **Test class methods**: Testing instance methods of classes.
* **Test cleanup**: Using `yield` in fixtures to handle teardown logic.
* **Test coverage**: Using the `pytest-cov` plugin to measure test coverage.

These features of `pytest` help you write flexible, efficient, and maintainable tests for your Python code.
