Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Tutorial

Getting Started with pytest for Unit Testing in Python

Learn how to write and run effective unit tests in Python using pytest. This beginner-friendly guide covers fixtures, edge cases, and practical examples to help you build confidence in your code.

July 2026 10 min read 2 views 0 hearts

Why Your Code Needs a Safety Net (And Why pytest Is the Best One)

If you've ever spent hours tracking down a bug that only appeared after you thought your code was perfect, you're not alone. I remember my first big Python project at PythonSkillset—I was so confident, only to watch it crash when a user entered an empty string. That's when I learned about unit testing, and specifically, about pytest.

What Exactly Is Unit Testing?

Think of unit testing like checking each individual Lego brick before you build a castle. You test the smallest pieces of your code—functions, methods, classes—in isolation to make sure they work correctly. Unit tests catch problems early, save you from embarrassing production bugs, and make refactoring much less scary.

Why pytest Instead of Python's Built-in unittest?

Python comes with a testing module called unittest, but many developers switch to pytest for good reasons:

  • Less boilerplate code – No need to create test classes or use special assertion methods
  • Better error messages – When a test fails, pytest tells you exactly what went wrong
  • Simple fixtures – Reusable test data without complicated setup code
  • Great plugin ecosystem – Code coverage, parallel testing, and more

Getting Started with pytest

Installation

First, install pytest using pip:

pip install pytest

Once installed, create a file named test_anything.py. pytest automatically discovers tests in files starting with test_ or ending with _test.

Your First Test

Let's say you have a simple function in a file called calculator.py:

def add(a, b):
    return a + b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

Now create test_calculator.py:

from calculator import add, divide

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0

def test_divide():
    assert divide(10, 2) == 5
    assert divide(3, 2) == 1.5

def test_divide_by_zero():
    import pytest
    with pytest.raises(ValueError):
        divide(5, 0)

Run your tests:

pytest

You'll see something like:

collected 3 items
test_calculator.py ...                                          [100%]

All green. Feels good, right?

Testing Edge Cases Like a Pro

Beginners often only test the "happy path"—when everything works perfectly. But real bugs hide in the edge cases. Here's how to think like a tester:

String Functions Example

def capitalize_name(name):
    if not isinstance(name, str):
        raise TypeError("Name must be a string")
    if len(name.strip()) == 0:
        raise ValueError("Name cannot be empty")
    return name.strip().title()

Tests for this function should check:

def test_normal_name():
    assert capitalize_name("john") == "John"

def test_multiple_words():
    assert capitalize_name("john doe") == "John Doe"

def test_already_capitalized():
    assert capitalize_name("John") == "John"

def test_with_spaces():
    assert capitalize_name("  alice  ") == "Alice"

def test_empty_string():
    import pytest
    with pytest.raises(ValueError):
        capitalize_name("   ")

def test_non_string_input():
    import pytest
    with pytest.raises(TypeError):
        capitalize_name(123)

Using Fixtures for Cleaner Tests

Fixtures are reusable pieces of test data or setup. They save you from repeating code and make tests easier to read.

import pytest

# Define a fixture
@pytest.fixture
def sample_user_data():
    return {
        "name": "Alice",
        "age": 30,
        "email": "alice@example.com"
    }

# Use the fixture in your test
def test_user_age(sample_user_data):
    assert sample_user_data["age"] == 30

def test_user_email(sample_user_data):
    assert "@" in sample_user_data["email"]

Fixtures can also clean up after themselves using yield:

@pytest.fixture
def temporary_file():
    file = open("test_data.txt", "w")
    file.write("temporary content")
    file.close()

    file = open("test_data.txt", "r")
    yield file

    import os
    os.remove("test_data.txt")

Grouping Tests with Classes

While pytest works fine with plain functions, you can also group related tests in a class:

class TestStringOperations:
    def test_uppercase(self):
        assert "hello".upper() == "HELLO"

    def test_reverse(self):
        assert "abc"[::-1] == "cba"

    def test_strip(self):
        assert "  spaced  ".strip() == "spaced"

Real-World Example from PythonSkillset

At PythonSkillset, we once had a function that processed user registration data. Here's how we tested it:

def validate_registration(username, email, age):
    errors = []
    if len(username) < 3:
        errors.append("Username must be at least 3 characters")
    if "@" not in email:
        errors.append("Invalid email address")
    if age < 13:
        errors.append("Must be at least 13 years old")
    return errors

def test_valid_registration():
    errors = validate_registration("john_doe", "john@example.com", 25)
    assert errors == []

def test_short_username():
    errors = validate_registration("jo", "john@example.com", 25)
    assert "Username must be at least 3 characters" in errors

def test_invalid_email():
    errors = validate_registration("john_doe", "notanemail", 25)
    assert "Invalid email address" in errors

def test_underage_user():
    errors = validate_registration("john_doe", "john@example.com", 10)
    assert "Must be at least 13 years old" in errors

def test_multiple_errors():
    errors = validate_registration("jo", "bad", 10)
    assert len(errors) == 3

Common Beginner Mistakes to Avoid

  1. Testing too much in one test – Keep each test focused on one behavior
  2. Forgetting to test error cases – Your code will handle bad input eventually
  3. Hardcoding values – Use different inputs to make sure tests aren't just luck
  4. Not running tests regularly – Make it a habit after every change
  5. Ignoring test failures – Red tests should be fixed immediately

Running Tests Efficiently

As your project grows, you'll want more control:

# Run specific test file
pytest test_calculator.py

# Run tests with more details
pytest -v

# Stop after first failure
pytest -x

# Run tests matching a pattern
pytest -k "divide"

# Generate code coverage report (install pytest-cov first)
pytest --cov=.

Final Thoughts

Unit testing with pytest isn't just about catching bugs—it's about giving yourself confidence to make changes, refactor code, and sleep better at night. Start small, test the critical paths first, and gradually build up your test suite. Your future self (and anyone else working on your code) will thank you.

At PythonSkillset, we've seen teams transform their development workflow once they embrace testing. And the best part? pytest makes it almost enjoyable. Almost.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.