Testing Environment Setup

Chapter 0: Testing Environment Setup

Introduction

Before writing effective tests, you need a solid testing environment. Like setting up a workshop, having the right tools organized makes all the difference. This chapter covers everything needed to write, run, and manage Python tests professionally.

We'll use pytest, Python's most powerful testing framework. While Python includes unittest in its standard library, pytest offers intuitive syntax, better error messages, powerful plugins, and a thriving ecosystem. Most modern Python projects choose pytest.

By the end of this chapter, you'll have:

  • pytest installed and configured
  • A proper Python virtual environment
  • A well-structured project layout
  • Configuration files for pytest
  • The ability to run tests from the command line

Installing pytest

Why pytest?

Pytest stands out for several reasons:

  • Simple syntax: Tests look like regular functions
  • Powerful assertions: Just use assert, no special methods needed
  • Rich plugin ecosystem: pytest-cov, pytest-mock, pytest-xdist, and hundreds more
  • Excellent error messages: When tests fail, pytest shows exactly what went wrong
  • Backwards compatible: Works with unittest tests too

Installing with pip

The simplest way to install pytest is with pip, Python's package installer:

pip install pytest

For specific versions or additional tools:

# Install specific version
pip install pytest==7.4.3

# Install with coverage support
pip install pytest pytest-cov

# Install with mocking support
pip install pytest pytest-mock

Verifying Installation

Let's verify pytest is installed correctly and check the version:

Managing Dependencies

For production projects, always document your dependencies:

requirements.txt:

pytest==7.4.3
pytest-cov==4.1.0
pytest-mock==3.12.0

pyproject.toml (modern approach):

[project]
dependencies = [
    "pytest>=7.4.0",
    "pytest-cov>=4.1.0",
]

[project.optional-dependencies]
dev = [
    "pytest-mock>=3.12.0",
]

Virtual Environments

Why Virtual Environments Matter

Virtual environments isolate your project's dependencies from other Python projects. Without them, you might:

  • Install conflicting package versions
  • Pollute your system Python installation
  • Make your project hard to reproduce on other machines

Think of a virtual environment as a clean, isolated workspace for each project.

Creating a Virtual Environment

Python 3.3+ includes venv in the standard library:

# Create a virtual environment named 'venv'
python3 -m venv venv

# On Windows
python -m venv venv

This creates a venv/ directory containing:

  • A Python interpreter
  • pip and setuptools
  • Space for installed packages

Activating and Deactivating

On macOS/Linux:

# Activate
source venv/bin/activate

# Your prompt changes to show (venv)
(venv) $ python --version

# Deactivate
deactivate

On Windows:

# Activate
venv\Scripts\activate

# Deactivate
deactivate

Installing Dependencies in venv

Once activated, packages install only in this environment:

# Install pytest in the virtual environment
(venv) $ pip install pytest

# Install from requirements.txt
(venv) $ pip install -r requirements.txt

# Freeze current dependencies
(venv) $ pip freeze > requirements.txt

Best Practices

  • Always use virtual environments for projects
  • Never commit venv/ to git (add it to .gitignore)
  • Document dependencies in requirements.txt or pyproject.toml
  • Use consistent naming: venv, .venv, or env

Project Structure

Standard Python Project Layout

A well-organized project makes testing easier. Here's a typical structure:

myproject/
ā”œā”€ā”€ src/
│   └── myproject/
│       ā”œā”€ā”€ __init__.py
│       ā”œā”€ā”€ core.py
│       └── utils.py
ā”œā”€ā”€ tests/
│   ā”œā”€ā”€ __init__.py
│   ā”œā”€ā”€ test_core.py
│   └── test_utils.py
ā”œā”€ā”€ venv/
ā”œā”€ā”€ pytest.ini
ā”œā”€ā”€ requirements.txt
ā”œā”€ā”€ setup.py
└── README.md

Test Directory Organization

Pytest supports two common patterns:

1. Separate tests/ directory (recommended):

project/
ā”œā”€ā”€ src/myproject/
│   └── module.py
└── tests/
    └── test_module.py

2. Tests alongside code:

myproject/
ā”œā”€ā”€ module.py
└── test_module.py

The separate directory keeps production code clean and makes it easy to exclude tests from distribution.

Naming Conventions

Pytest discovers tests by name:

  • Test files: test_*.py or *_test.py
  • Test classes: Test* (capital T)
  • Test functions: test_*

Examples:

# test_calculator.py
def test_addition():
    assert 2 + 2 == 4

class TestCalculator:
    def test_subtraction(self):
        assert 5 - 3 == 2

The init.py File

Modern Python (3.3+) doesn't require __init__.py for regular packages, but it's still useful for test directories:

# tests/__init__.py
# Can be empty, or contain test fixtures/utilities

pytest Configuration

Configuration Files

Pytest can be configured via:

  • pytest.ini (dedicated pytest config)
  • pyproject.toml (modern Python projects)
  • setup.cfg (legacy projects)

pytest.ini Basics

Create pytest.ini in your project root:

[pytest]
# Where to find tests
testpaths = tests

# Test file patterns
python_files = test_*.py *_test.py

# Test class patterns
python_classes = Test*

# Test function patterns
python_functions = test_*

# Minimum Python version
minversion = 7.0

# Add options to every pytest run
addopts =
    -v
    --strict-markers
    --tb=short

pyproject.toml Configuration

For modern projects using pyproject.toml:

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
    "-v",
    "--strict-markers",
    "--cov=src",
]

Common Configuration Options

  • testpaths: Directories to search for tests
  • python_files: File patterns to match
  • addopts: Default command-line options
  • markers: Custom test markers
  • minversion: Minimum pytest version required

Custom Markers

Define custom markers to categorize tests:

[pytest]
markers =
    slow: marks tests as slow
    integration: marks tests as integration tests
    unit: marks tests as unit tests

Use them in tests:

import pytest

@pytest.mark.slow
def test_complex_operation():
    # ... slow test
    pass

Run specific markers:

pytest -m "not slow"  # Skip slow tests
pytest -m integration  # Run only integration tests

Running Your First Test

Creating a Simple Test

Create test_basic.py in your tests directory:

# tests/test_basic.py
def test_addition():
    """Test that addition works correctly."""
    result = 2 + 2
    assert result == 4

def test_string_concatenation():
    """Test string concatenation."""
    result = "Hello" + " " + "World"
    assert result == "Hello World"

def test_list_operations():
    """Test list operations."""
    numbers = [1, 2, 3]
    numbers.append(4)
    assert len(numbers) == 4
    assert 4 in numbers

Running pytest

From your project directory:

# Run all tests
pytest

# Run with verbose output
pytest -v

# Run specific file
pytest tests/test_basic.py

# Run specific test
pytest tests/test_basic.py::test_addition

Understanding pytest Output

When you run pytest, you'll see:

============================= test session starts ==============================
platform linux -- Python 3.11.0, pytest-7.4.3, pluggy-1.3.0
rootdir: /home/user/myproject
collected 3 items

tests/test_basic.py ...                                                  [100%]

============================== 3 passed in 0.02s ===============================

Each . represents a passing test. Failed tests show F, skipped tests show s.

Verbose Mode

Use -v for detailed output:

pytest -v

# Output shows each test name:
tests/test_basic.py::test_addition PASSED                             [ 33%]
tests/test_basic.py::test_string_concatenation PASSED                 [ 66%]
tests/test_basic.py::test_list_operations PASSED                      [100%]

Test Discovery

How pytest Finds Tests

Pytest automatically discovers tests using these rules:

  1. Search directories in testpaths (or current directory)
  2. Look for files matching test_*.py or *_test.py
  3. Inside files, find:
    • Functions matching test_*
    • Classes matching Test*
    • Methods matching test_* inside Test* classes

Naming Conventions Deep Dive

āœ… Valid test names:

# Files
test_user.py
test_api.py
calculator_test.py

# Functions
def test_login():
    pass

def test_logout():
    pass

# Classes
class TestUser:
    def test_creation(self):
        pass

āŒ Invalid test names (pytest won't discover):

# Files
user_tests.py  # Wrong pattern

# Functions
def check_login():  # Doesn't start with test_
    pass

# Classes
class UserTests:  # Doesn't start with Test
    pass

Running Specific Tests

Target tests precisely:

# Run all tests in a directory
pytest tests/unit/

# Run a specific file
pytest tests/test_api.py

# Run a specific function
pytest tests/test_api.py::test_get_user

# Run a specific method in a class
pytest tests/test_api.py::TestUserAPI::test_create_user

# Run tests matching a pattern
pytest -k "user"  # Runs all tests with "user" in the name
pytest -k "not slow"  # Skip tests with "slow" in name

Custom Discovery Patterns

Override default patterns in pytest.ini:

[pytest]
# Find tests in different directories
testpaths = tests integration_tests

# Match different file patterns
python_files = test_*.py check_*.py *_test.py

# Match different function patterns
python_functions = test_* check_*

Skipping Tests

Mark tests to skip:

import pytest

@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
    pass

@pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10+")
def test_new_feature():
    pass

Next Steps

You now have a complete testing environment! You've learned how to:

  • āœ… Install pytest and verify it works
  • āœ… Create virtual environments for isolation
  • āœ… Structure Python projects for testing
  • āœ… Configure pytest with pytest.ini
  • āœ… Run tests from the command line
  • āœ… Understand how pytest discovers tests

In the next chapter, we'll dive into Unit Testing Fundamentals, where you'll learn to write comprehensive test suites using pytest's full power.

Course Recommendations

Ready to take your Python testing skills further? Check out these courses on paiml.com:

Python Testing with pytest

  • Complete pytest ecosystem coverage
  • Test-Driven Development workflows
  • Fixtures, parametrization, and plugins
  • Real-world testing patterns
  • Enroll at paiml.com

DevOps for Python Developers

  • CI/CD integration with GitHub Actions
  • Automated testing pipelines
  • Code quality tools and enforcement
  • Docker and containerization for testing
  • Enroll at paiml.com

Professional Python Development

  • Complete development environment setup
  • Virtual environments and dependency management
  • Project structure best practices
  • Testing strategies for production code
  • Enroll at paiml.com

Quiz