Python

P
PythonHub
Progress 0%

Python

  • Home
  • History of Python
  • Applications of Python
  • Introduction To Python
    • What is Python
    • Character Set
    • Tokens in Python
    • Python Execution Mode
    • Variable And Identifiers
    • Data Types in Python
    • Operators And Expressions
    • Constants in Python
    • Assignment Statement
    • Input / Output in Python
    • Simple Python Scripts
    • Namespace in Python
    • πŸ“ Assignments
  • Operators in Python
    • Arithmetic Operators
    • Assignment Operators
    • Shorthand Assignment Operators
    • Relational Operators
    • Logical Operators
    • Bitwise Operators
    • Special Operators
    • πŸ“ Assignments
  • Input Output in Python
    • Accept Input
    • Output Formatting
    • πŸ“ Assignments
  • Conditional Statement
    • Decision Making
    • if Statement
    • IF-ELSE STATEMENT
    • IF-ELSE LADDER
    • NESTED IF-ELSE
    • Short Hand IF-ELSE
    • πŸ“ Assignments
  • Loops
    • Introduction to Loops
    • While Loop
    • Nested While Loop
    • πŸ“ While Loop Assignments
    • For Loop
    • For Loop Examples
    • Nested For Loop
    • Nested For Loop Examples
    • Infinite While Loops
    • Infinite For Loops
    • Break, Continue and Else in Loops
    • Difference Between For and While Loop
    • For Each Loop
    • πŸ“ For Each Assignments
    • πŸ“ All Loops Assignments
  • List
    • List in Python
    • Access List Elements
    • List Functions
    • Iterate (Loop) List
    • List Comprehension
    • πŸ“ Assignments
  • Tuple
    • Tuple in Python
    • Access Tuple Elements
    • Tuple Functions
    • Iterate (Loop) Tuple
    • Unpack Tuple
    • Tuple Comprehension
    • πŸ“ Assignments
  • Set
    • Set in Python
    • Access Set Elements
    • Set Methods
    • Iterate (Loop) Set
    • Pack/Unpack Set
    • Set Comprehension
    • πŸ“ Assignments
  • Dictionary
    • Dictionary
    • Access Dictionary Items
    • Dictionary Methods
    • Iterate (Loop) Dictionary
    • Formatting Dictionaries
    • Nested Dictionaries
    • Dictionary Comprehension
    • πŸ“ Assignments
  • Diff List Tuple Set Dictionary
    • List vs Tuple
    • List vs Set
    • List vs Dictionary
    • Tuple vs Set
    • Tuple vs Dictionary
    • Dictionary vs Set
    • πŸ“ Assignments
  • Exception
    • Error vs Exception
    • Types of Exception
    • Exception Handling
    • User Defined Exception
    • Logging Exception
    • πŸ“ Assignments
  • Functions
    • Introduction to Functions
    • Modular Programming
    • Types of Functions
    • Inbuilt Functions
    • Need For User-Defined Function
    • Elements of User Defined Function
    • Function Arguments
    • Nesting of Functions
    • Recursion
    • Global Local and Non Local
    • Python Lambda Functions
    • πŸ“ Assignments
  • Python Module
    • Introduction to Module
    • Inbuilt Modules in Python
    • User Defined Module
    • πŸ“ Assignments
  • File Handling
    • Introduction to Files
    • Create File
    • Read Files
    • Write to File
    • Rename File
    • Copy File
    • Move File
    • List Files in Directory
    • Binary Files
    • Zipping and Unzipping Files
    • πŸ“ Assignments
  • Strings
    • Basics of Strings
    • String Special Operators
    • String Formatting Operators
    • String Methods
    • πŸ“ Assignments
  • Regular Expressions
  • Python OOPS
    • Basics of Object Oriented
    • What are Classes and Objects?
    • Creating Class and Object
    • OOP vs Procedural Programming
    • Difference Between Classes and Objects
    • Constructors
    • Destructor
    • Built Class Methods and Attributes
    • Class and Instance Variables
    • Inheritance in Python
    • Single Inheritance
    • Multiple Inheritance
    • Multilevel Inheritance
    • Hierarchical Inheritance
    • Hybrid Inheritance
    • Abstraction
    • Method Overriding
    • Abstract Method
    • Interfaces in Python
    • Abstract Class vs Interface
    • Public, Private and Protected
    • Overloading vs Overriding
    • Inheritance vs Composition
    • Encapsulation
    • Polymorphism
    • Inner Classes
    • πŸ“ Assignments
  • Advanced Python
    • Iterator in Python
    • Generator in Python
    • Decorator in Python
    • Type Hints in Python
    • Async/Await Programming
    • Dataclasses in Python
    • Context Managers in Python
    • Property Decorator in Python
    • __slots__ in Python
    • Enums in Python
    • Walrus Operator in Python
    • Match-Case in Python
    • πŸ“ Assignments
  • Python Standard Library
    • Collections Module
    • Itertools Module
    • Functools Module
    • Datetime Module
    • JSON Module
    • OS Module
    • Sys Module
    • Random Module
    • Math Module
    • πŸ“ Assignments
  • Python Testing
    • Unit Testing in Python
    • Pytest Framework
    • Mocking in Python
    • πŸ“ Assignments
  • Python Best Practices
    • PEP 8 Style Guide
    • Docstrings in Python
    • Logging in Python
    • Code Optimization Tips
    • Debugging Techniques
    • πŸ“ Assignments
  • MySQL Database in Python
    • Introduction to MySQL with Python
    • DBMS vs File System
    • Connecting to MySQL Database
    • Create Database in MySQL
    • Create Table in MySQL
    • Insert Data in MySQL
    • Insert Multiple Rows
    • Select Data from MySQL
    • WHERE Clause in MySQL
    • Update Data in MySQL
    • Delete Data from MySQL
    • Parameterized Queries
    • Transaction Management
    • Error Handling
    • Connection Pooling
    • MySQL Drivers Guide
    • Joins in MySQL
    • Aggregation Functions
    • Backup MySQL Database
    • Best Practices
    • πŸ“ Assignments
  • MySQL Database Operations
    • SELECT Statement
    • MySQL Operators
    • DDL Statements
    • DML Statements
    • Subqueries
    • JOIN Operations
    • Aggregation
    • Case Study
    • πŸ“ Assignments
  • Graphics in Python
  • Threads in Python
    • Introduction to Threads
    • Process vs Threads
    • Concurrent Programming & GIL
    • Uses of Threads
    • Creating Threads
    • Single Tasking
    • Multi Tasking
    • Thread Synchronization
    • πŸ“ Assignments
  • Interview Questions & Answers
  • Python Case Studies
  • Multiple Choice Questions
  • πŸ“ Practice Problems
Get in Touch
  • tech2dsm@gmail.com

© Sankalan Data Tech

Python Language Interactive Tutorial

Python: Pytest Framework

Python Pytest Framework - Complete Guide

Write better tests with the pytest framework.

Created by Sankalan Data Tech Team Verified
Data Engineers, Analysts, Scientists & Trainers
Created by experienced Python developers, data engineers, and data scientists to make programming easy through practical examples, real-world experience, and clear explanations.
On this page:
  • What is Pytest?
  • Why Pytest?
  • Your First Pytest
  • Fixtures in Pytest
  • Parametrize Tests
  • Marks and Skipping
  • Real-World Example
  • Best Practices
  • Try It Yourself
  • Quick Quiz
  • Frequently Asked Questions
Share this tutorial:
Twitter LinkedIn Facebook WhatsApp Reddit Telegram Email Copy Link
What You'll Learn Here
  • What is pytest β€” a powerful testing framework
  • Why use pytest β€” simpler, cleaner tests
  • Writing tests β€” no classes required
  • Fixtures β€” setup and teardown made easy
  • Parametrize β€” test multiple inputs
  • Marks β€” organize and skip tests

What is Pytest?

Pytest is a popular testing framework for Python. It's simpler and more powerful than the built-in unittest module. Many developers prefer pytest because it requires less boilerplate code and has useful features like fixtures and parameterized testing.

Think of pytest like a modern testing toolkit. While unittest is like a basic toolset, pytest is like a complete workshop with specialized tools for every testing need. It's fast, flexible, and makes testing fun.

To use pytest, you need to install it first: pip install pytest

πŸ’‘ Key concept: Pytest is a mature testing framework that makes writing and running tests simple and enjoyable.

Why Pytest?

1

The Benefits of Pytest

Pytest offers several advantages over the built-in unittest module.

# Why Pytest?

print("=" * 50)
print("WHY PYTEST?")
print("=" * 50)

# ============================================================
# UNITTEST STYLE (More Code)
# ============================================================

print("\n1. UNITTEST STYLE")

print("""
import unittest

class TestMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)
    
    def test_subtract(self):
        self.assertEqual(subtract(5, 3), 2)

if __name__ == '__main__':
    unittest.main()
""")


# ============================================================
# PYTEST STYLE (Less Code)
# ============================================================

print("\n2. PYTEST STYLE")

print("""
def test_add():
    assert add(2, 3) == 5

def test_subtract():
    assert subtract(5, 3) == 2
""")

print("   No classes needed!")
print("   No assert methods needed!")


# ============================================================
# BENEFITS SUMMARY
# ============================================================

print("\n" + "-" * 30)
print("BENEFITS OF PYTEST")
print("-" * 30)
print("""
- No classes required (just functions)
- Uses plain assert statements
- Detailed failure reports
- Powerful fixtures
- Parametrize tests easily
- Many plugins available
- Automatic test discovery
- Works with unittest tests too
""")

Benefits of pytest:

  • No classes needed β€” just write functions
  • Plain asserts β€” use assert statements
  • Better output β€” detailed failure reports
  • Powerful fixtures β€” more flexible than setUp
  • Parametrize β€” test multiple inputs easily

Quick Check: What's the main advantage of pytest over unittest? (Answer: Less boilerplate code and more powerful features)

Your First Pytest

2

Writing and Running Your First Test

Writing a pytest is as simple as writing a function that starts with test_ and uses assert statements.

# Your First Pytest

print("=" * 50)
print("YOUR FIRST PYTEST")
print("=" * 50)

# ============================================================
# CODE TO TEST
# ============================================================

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

def multiply(a, b):
    return a * b

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


# ============================================================
# PYTEST TESTS (Save in test_calculator.py)
# ============================================================

print("\n1. PYTEST TESTS")

print("""
# test_calculator.py

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

def test_multiply():
    assert multiply(2, 3) == 6
    assert multiply(-2, 3) == -6
    assert multiply(0, 5) == 0

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

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


# ============================================================
# RUNNING TESTS
# ============================================================

print("\n2. RUNNING TESTS")

print("""
Run tests:
    pytest test_calculator.py

Run with verbose output:
    pytest -v test_calculator.py

Run all tests in directory:
    pytest

Run specific test:
    pytest test_calculator.py::test_add
""")


# ============================================================
# EXAMPLE OUTPUT
# ============================================================

print("\n3. EXAMPLE OUTPUT")

print("""
$ pytest test_calculator.py -v

============================= test session starts ==============================
collected 4 items

test_calculator.py::test_add PASSED                                        [ 25%]
test_calculator.py::test_multiply PASSED                                   [ 50%]
test_calculator.py::test_divide PASSED                                     [ 75%]
test_calculator.py::test_divide_by_zero PASSED                             [100%]

============================== 4 passed in 0.02s ===============================
""")


# ============================================================
# ASSERTION DETAILS
# ============================================================

print("\n4. ASSERTION DETAILS")

print("""
If a test fails, pytest shows detailed information:
    def test_add():
        assert add(2, 3) == 6  # Fails!

Output:
    E       assert 5 == 6
    E        +  where 5 = add(2, 3)
""")

First pytest key points:

  • Test functions β€” start with test_
  • Assert statements β€” use plain assert
  • Run tests β€” pytest filename.py
  • pytest.raises β€” test for exceptions

Quick Check: How do you test for exceptions in pytest? (Answer: with pytest.raises(ExceptionType):)

Fixtures in Pytest

3

Better Setup and Teardown

Pytest fixtures are more powerful than unittest's setUp/tearDown. They can be used across multiple tests and can have different scopes.

# Fixtures in Pytest

print("=" * 50)
print("FIXTURES IN PYTEST")
print("=" * 50)

# ============================================================
# CODE TO TEST
# ============================================================

class Calculator:
    def __init__(self):
        self.result = 0
    
    def add(self, x):
        self.result += x
        return self.result
    
    def subtract(self, x):
        self.result -= x
        return self.result
    
    def reset(self):
        self.result = 0
        return self.result


# ============================================================
# PYTEST WITH FIXTURES
# ============================================================

print("\n1. FIXTURES EXAMPLE")

print("""
import pytest

# Fixture - runs before each test
@pytest.fixture
def calculator():
    \"\"\"Create a calculator with initial state\"\"\"
    print("   Creating calculator")
    calc = Calculator()
    yield calc  # This is what tests receive
    print("   Cleaning up")

# Tests using the fixture
def test_add(calculator):
    assert calculator.add(5) == 5
    assert calculator.add(3) == 8

def test_subtract(calculator):
    assert calculator.subtract(10) == -10
    assert calculator.add(5) == -5

def test_reset(calculator):
    calculator.add(10)
    assert calculator.result == 10
    calculator.reset()
    assert calculator.result == 0
""")


# ============================================================
# FIXTURE SCOPES
# ============================================================

print("\n2. FIXTURE SCOPES")

print("""
@pytest.fixture(scope="function")  # Default - runs per test
@pytest.fixture(scope="class")     # Runs once per test class
@pytest.fixture(scope="module")    # Runs once per module
@pytest.fixture(scope="session")   # Runs once per test session

# Example:
@pytest.fixture(scope="module")
def db_connection():
    # This runs once for all tests in the module
    print("   Connecting to database")
    connection = "DB Connection"
    yield connection
    print("   Disconnecting from database")
""")


# ============================================================
# USING MULTIPLE FIXTURES
# ============================================================

print("\n3. USING MULTIPLE FIXTURES")

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

@pytest.fixture
def logged_in_user(user):
    # Use one fixture to create another
    return {"user": user, "logged_in": True}

def test_user_profile(logged_in_user):
    assert logged_in_user["user"]["name"] == "Alice"
    assert logged_in_user["logged_in"] is True
""")


# ============================================================
# FIXTURE YIELD (Cleanup)
# ============================================================

print("\n4. FIXTURE CLEANUP (yield)")

print("""
@pytest.fixture
def temp_file():
    # Setup
    with open("temp.txt", "w") as f:
        f.write("test data")
    yield "temp.txt"  # Provide the file to tests
    # Teardown (after test runs)
    import os
    if os.path.exists("temp.txt"):
        os.remove("temp.txt")
""")


# ============================================================
# CONFTEST.PY (Shared Fixtures)
# ============================================================

print("\n5. CONFTEST.PY")

print("""
# Create a file called conftest.py in your test directory
# Fixtures defined here are available to all tests

# conftest.py
import pytest

@pytest.fixture
def global_fixture():
    return "Available everywhere"

# Any test can use this fixture without importing
""")

Fixtures key points:

  • @pytest.fixture β€” decorator to create a fixture
  • yield β€” provides the value and runs cleanup after
  • Scopes β€” function, class, module, session
  • conftest.py β€” share fixtures across test files

Quick Check: What does yield do in a pytest fixture? (Answer: It provides the fixture value to tests and runs cleanup code after the test)

Parametrize Tests

4

Test Multiple Inputs Easily

@pytest.mark.parametrize lets you run the same test with different inputs. This is much cleaner than writing separate tests.

# Parametrize Tests

print("=" * 50)
print("PARAMETRIZE TESTS")
print("=" * 50)

# ============================================================
# CODE TO TEST
# ============================================================

def is_even(n):
    return n % 2 == 0

def to_uppercase(text):
    return text.upper()

def factorial(n):
    if n < 0:
        raise ValueError("Negative factorial")
    if n == 0:
        return 1
    return n * factorial(n - 1)


# ============================================================
# PARAMETRIZE EXAMPLES
# ============================================================

print("\n1. BASIC PARAMETRIZE")

print("""
import pytest

# Test multiple values in one test
@pytest.mark.parametrize("number, expected", [
    (2, True),
    (3, False),
    (4, True),
    (5, False),
    (10, True),
    (11, False),
])
def test_is_even(number, expected):
    assert is_even(number) == expected
""")


# ============================================================
# MULTIPLE PARAMETERS
# ============================================================

print("\n2. MULTIPLE PARAMETERS")

print("""
@pytest.mark.parametrize("input_text, expected", [
    ("hello", "HELLO"),
    ("World", "WORLD"),
    ("Python", "PYTHON"),
    ("123", "123"),
])
def test_to_uppercase(input_text, expected):
    assert to_uppercase(input_text) == expected
""")


# ============================================================
# COMBINED PARAMETRIZE
# ============================================================

print("\n3. COMBINED PARAMETRIZE")

print("""
# Combine multiple parametrize decorators
@pytest.mark.parametrize("n", [0, 1, 2, 3, 4, 5])
@pytest.mark.parametrize("expected", [1, 1, 2, 6, 24, 120])
def test_factorial(n, expected):
    assert factorial(n) == expected

# Or use with tuples
@pytest.mark.parametrize("n, expected", [
    (0, 1),
    (1, 1),
    (2, 2),
    (3, 6),
    (4, 24),
    (5, 120),
])
def test_factorial(n, expected):
    assert factorial(n) == expected
""")


# ============================================================
# PARAMETRIZE WITH EXCEPTIONS
# ============================================================

print("\n4. PARAMETRIZE WITH EXCEPTIONS")

print("""
@pytest.mark.parametrize("n, expected_exception", [
    (-1, ValueError),
    (-5, ValueError),
    (0, None),  # No exception
    (1, None),
])
def test_factorial_exceptions(n, expected_exception):
    if expected_exception:
        with pytest.raises(expected_exception):
            factorial(n)
    else:
        assert factorial(n) is not None
""")


# ============================================================
# PARAMETRIZE WITH IDS
# ============================================================

print("\n5. PARAMETRIZE WITH IDS")

print("""
# Add IDs to test cases for better reporting
@pytest.mark.parametrize("n, expected", [
    (0, 1),
    (1, 1),
    (2, 2),
    (3, 6),
    (4, 24),
    (5, 120),
], ids=["zero", "one", "two", "three", "four", "five"])
def test_factorial(n, expected):
    assert factorial(n) == expected
""")

Parametrize key points:

  • @pytest.mark.parametrize β€” run test with multiple inputs
  • Arguments β€” list of (input, expected) tuples
  • Multiple parameters β€” can have multiple inputs
  • ids β€” name test cases for better output

Quick Check: What decorator lets you run a test with multiple inputs? (Answer: @pytest.mark.parametrize)

Marks and Skipping

5

Organize, Skip, and Select Tests

Pytest marks let you categorize tests, skip tests, and run specific groups of tests.

# Marks and Skipping

print("=" * 50)
print("MARKS AND SKIPPING")
print("=" * 50)

# ============================================================
# MARKING TESTS
# ============================================================

print("\n1. MARKING TESTS")

print("""
import pytest

# Mark a test as slow
@pytest.mark.slow
def test_heavy_computation():
    import time
    time.sleep(2)
    assert True

# Mark a test as integration test
@pytest.mark.integration
def test_database_connection():
    assert True

# Mark a test with multiple tags
@pytest.mark.slow
@pytest.mark.integration
def test_slow_integration():
    assert True
""")


# ============================================================
# SKIPPING TESTS
# ============================================================

print("\n2. SKIPPING TESTS")

print("""
# Skip a test unconditionally
@pytest.mark.skip(reason="Not implemented yet")
def test_unimplemented():
    assert False

# Skip a test conditionally
import sys
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Requires Python 3.8+")
def test_python_version():
    assert True

# Skip based on a condition
@pytest.mark.skipif(not hasattr(os, "getlogin"), reason="No getlogin")
def test_user_login():
    assert True
""")


# ============================================================
# EXPECTED FAILURES (xfail)
# ============================================================

print("\n3. EXPECTED FAILURES (xfail)")

print("""
# Mark a test as expected to fail
@pytest.mark.xfail(reason="Known bug")
def test_buggy_function():
    assert False  # This will show as xfail (expected failure)

# Conditional xfail
@pytest.mark.xfail(sys.platform == "win32", reason="Not supported on Windows")
def test_platform_specific():
    assert True
""")


# ============================================================
# RUNNING TESTS BY MARK
# ============================================================

print("\n4. RUNNING TESTS BY MARK")

print("""
# Run only slow tests
pytest -m slow

# Run only tests with both slow and integration
pytest -m "slow and integration"

# Run all except slow tests
pytest -m "not slow"

# Run tests with multiple marks
pytest -m "slow or integration"
""")


# ============================================================
# CUSTOM MARKS REGISTRATION
# ============================================================

print("\n5. CUSTOM MARKS REGISTRATION")

print("""
# Register custom marks in pytest.ini:
# [pytest]
# markers =
#     slow: marks tests as slow
#     integration: marks tests as integration tests
#     unit: marks tests as unit tests
""")


# ============================================================
# SUMMARY OF MARKS
# ============================================================

print("\n" + "-" * 30)
print("MARKS SUMMARY")
print("-" * 30)
print("""
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Mark                β”‚ Purpose                                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ @pytest.mark.skip   β”‚ Skip test unconditionally                 β”‚
β”‚ @pytest.mark.skipif β”‚ Skip test if condition is true            β”‚
β”‚ @pytest.mark.xfail  β”‚ Test is expected to fail                  β”‚
β”‚ @pytest.mark.param  β”‚ Run test with multiple parameters         β”‚
β”‚ @pytest.mark.slow   β”‚ Custom mark (any name)                    β”‚
β”‚ @pytest.mark.timeoutβ”‚ Set timeout (requires plugin)             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
""")

Marks key points:

  • @pytest.mark.skip β€” skip a test
  • @pytest.mark.skipif β€” skip conditionally
  • @pytest.mark.xfail β€” expected failure
  • Custom marks β€” categorize tests
  • Run with -m β€” select tests by mark

Quick Check: How do you run only tests marked as "slow"? (Answer: pytest -m slow)

Real-World Example

6

Testing a User Authentication System

# Real-World Example: Testing User Authentication

import pytest
import hashlib
import time

print("=" * 60)
print("TESTING USER AUTHENTICATION SYSTEM")
print("=" * 60)

# ============================================================
# CODE TO TEST
# ============================================================

class UserAuth:
    """Simple user authentication system"""
    
    def __init__(self):
        self.users = {}
        self.sessions = {}
        self.failed_attempts = {}
    
    def _hash_password(self, password):
        return hashlib.sha256(password.encode()).hexdigest()[:16]
    
    def register_user(self, username, password):
        if username in self.users:
            raise ValueError(f"User {username} already exists")
        if len(password) < 8:
            raise ValueError("Password must be at least 8 characters")
        self.users[username] = self._hash_password(password)
        self.failed_attempts[username] = 0
        return f"User {username} registered"
    
    def login(self, username, password):
        if username not in self.users:
            raise ValueError(f"User {username} not found")
        
        if self.failed_attempts.get(username, 0) >= 3:
            return "Account locked due to too many failed attempts"
        
        if self.users[username] == self._hash_password(password):
            self.failed_attempts[username] = 0
            self.sessions[username] = int(time.time())
            return "Login successful"
        else:
            self.failed_attempts[username] = self.failed_attempts.get(username, 0) + 1
            return "Invalid password"
    
    def logout(self, username):
        if username in self.sessions:
            del self.sessions[username]
            return "Logged out"
        return "User not logged in"
    
    def is_logged_in(self, username):
        return username in self.sessions
    
    def get_failed_attempts(self, username):
        return self.failed_attempts.get(username, 0)


# ============================================================
# PYTEST TESTS
# ============================================================

print("\n1. TEST FILE (test_auth.py)")

print("""
import pytest

@pytest.fixture
def auth():
    \"\"\"Create a fresh auth system for each test\"\"\"
    return UserAuth()

def test_register_user(auth):
    result = auth.register_user("alice", "SecurePass123")
    assert result == "User alice registered"
    assert "alice" in auth.users

def test_register_duplicate_user(auth):
    auth.register_user("alice", "SecurePass123")
    with pytest.raises(ValueError, match="User alice already exists"):
        auth.register_user("alice", "NewPass456")

def test_register_short_password(auth):
    with pytest.raises(ValueError, match="Password must be at least 8 characters"):
        auth.register_user("bob", "short")

def test_login_success(auth):
    auth.register_user("alice", "SecurePass123")
    result = auth.login("alice", "SecurePass123")
    assert result == "Login successful"
    assert auth.is_logged_in("alice") is True

def test_login_wrong_password(auth):
    auth.register_user("alice", "SecurePass123")
    result = auth.login("alice", "WrongPassword")
    assert result == "Invalid password"
    assert auth.get_failed_attempts("alice") == 1
    assert auth.is_logged_in("alice") is False

def test_login_lockout(auth):
    auth.register_user("alice", "SecurePass123")
    for _ in range(3):
        auth.login("alice", "WrongPassword")
    result = auth.login("alice", "SecurePass123")
    assert result == "Account locked due to too many failed attempts"

def test_logout(auth):
    auth.register_user("alice", "SecurePass123")
    auth.login("alice", "SecurePass123")
    result = auth.logout("alice")
    assert result == "Logged out"
    assert auth.is_logged_in("alice") is False

def test_login_user_not_found(auth):
    with pytest.raises(ValueError, match="User unknown not found"):
        auth.login("unknown", "password")

# Parametrized tests for multiple users
@pytest.mark.parametrize("username, password", [
    ("alice", "SecurePass123"),
    ("bob", "MyP@ssw0rd!"),
    ("charlie", "SecretPassword"),
])
def test_multiple_users(auth, username, password):
    auth.register_user(username, password)
    result = auth.login(username, password)
    assert result == "Login successful"
    assert auth.is_logged_in(username) is True
""")

Real-world example key points:

  • Fixture β€” creates a fresh auth system for each test
  • Test registration β€” register users with validation
  • Test login β€” success, failure, and lockout
  • Parametrize β€” test multiple users
  • Exception testing β€” test error cases

Quick Check: What would you use a fixture for in this example? (Answer: To create a fresh UserAuth instance for each test)

Best Practices

7

Writing Great Tests with Pytest

# Best Practices for Pytest

print("=" * 60)
print("BEST PRACTICES FOR PYTEST")
print("=" * 60)

# ============================================================
# 1. NAME TESTS CLEARLY
# ============================================================

print("\n1. NAME TESTS CLEARLY")

print("""
# Good - clear description
def test_login_with_correct_password_returns_success():
    ...

def test_login_with_wrong_password_returns_error():
    ...

# Bad - vague name
def test_login():
    ...
""")


# ============================================================
# 2. USE FIXTURES FOR COMMON SETUP
# ============================================================

print("\n2. USE FIXTURES FOR COMMON SETUP")

print("""
# Good - use fixture
@pytest.fixture
def user():
    return {"name": "Alice", "email": "alice@example.com"}

def test_user_email(user):
    assert user["email"] == "alice@example.com"

# Bad - repeat setup in each test
def test_user_email():
    user = {"name": "Alice", "email": "alice@example.com"}
    assert user["email"] == "alice@example.com"
""")


# ============================================================
# 3. USE PARAMETRIZE FOR MULTIPLE CASES
# ============================================================

print("\n3. USE PARAMETRIZE FOR MULTIPLE CASES")

print("""
# Good - one test with multiple cases
@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (0, 5, 5),
    (-2, 3, 1),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

# Bad - multiple similar tests
def test_add_positive():
    assert add(2, 3) == 5
def test_add_zero():
    assert add(0, 5) == 5
def test_add_negative():
    assert add(-2, 3) == 1
""")


# ============================================================
# 4. KEEP TESTS INDEPENDENT
# ============================================================

print("\n4. KEEP TESTS INDEPENDENT")

print("""
# Good - each test creates its own data
def test_user_creation():
    user = User("alice")
    assert user.name == "alice"

def test_user_email():
    user = User("alice", "alice@example.com")
    assert user.email == "alice@example.com"

# Bad - tests share state
user = User("alice")  # Global state
def test_user_creation():
    assert user.name == "alice"
def test_user_email():
    # This test depends on the previous one
    assert user.email == "alice@example.com"
""")


# ============================================================
# 5. USE APPROPRIATE ASSERTIONS
# ============================================================

print("\n5. USE APPROPRIATE ASSERTIONS")

print("""
# Good - use pytest's rich assertions
def test_result():
    result = calculate()
    assert result == 10
    assert "error" not in result
    assert len(result) > 5

# Bad - too many assertions
def test_result():
    result = calculate()
    assert result == 10
    assert result == 10
    assert result == 10  # Redundant
""")


# ============================================================
# 6. TEST EDGE CASES
# ============================================================

print("\n6. TEST EDGE CASES")

print("""
def test_divide_by_zero():
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

def test_empty_list():
    assert process([]) == []

def test_negative_numbers():
    assert process(-5) == "negative"
""")

# ============================================================
# 7. SUMMARY
# ============================================================

print("\n" + "=" * 60)
print("BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
- Name tests clearly and descriptively
- Use fixtures for common setup
- Use parametrize for multiple cases
- Keep tests independent
- Test edge cases and errors
- Use appropriate assertions
- Run tests frequently
""")

Best practices summary:

  • Clear names β€” describe what's being tested
  • Use fixtures β€” avoid repetition
  • Parametrize β€” test multiple cases
  • Independent tests β€” no shared state
  • Edge cases β€” test error conditions

Quick Check: Why should tests be independent? (Answer: So they can run in any order without affecting each other)

Try It Yourself

Experiment with pytest in the editor below.

Loading Pyodide... 0%
Python Code Editor
==================================================
PYTEST - PRACTICE
==================================================
πŸ†

You've Got It!

You now understand the pytest framework in Python. You know how to write tests, use fixtures, parametrize tests, and use marks to organize your tests.

Quick Quiz

Test what you've learned:

1. How do you install pytest?
2. How do you define a test in pytest?
3. What decorator is used to create a fixture?
4. What decorator lets you test multiple inputs?
5. How do you run only tests marked as "slow"?

Frequently Asked Questions

What is pytest in Python? β–Ό

Pytest is a powerful testing framework for Python. It makes writing and running tests easy with simple syntax, powerful features like fixtures and parametrize, and detailed test reports.

What's the difference between pytest and unittest? β–Ό

pytest is more modern and requires less boilerplate code. You don't need to create test classes or use specific assert methods. pytest also has more features like fixtures, parametrize, and powerful plugins.

How do I run pytest? β–Ό

Simply run pytest in your terminal. You can also specify a file: pytest test_file.py, a test function: pytest test_file.py::test_function, or use options like -v for verbose output.

What are fixtures in pytest? β–Ό

Fixtures are functions that provide test data or setup. They're used with @pytest.fixture decorator. Fixtures can be shared across tests, have different scopes, and can be parameterized.

How do I skip a test in pytest? β–Ό

Use @pytest.mark.skip(reason="message") to skip a test unconditionally, or @pytest.mark.skipif(condition, reason="message") to skip conditionally.

What is conftest.py used for? β–Ό

conftest.py is a special file where you can define fixtures that are available to all test files in the directory. It's a great way to share setup code across multiple test files.

Where to Go From Here

Now that you understand pytest, check out these related topics:

Mocking

Learn how to mock dependencies in your tests.

Learn More β†’

Unit Testing

Learn about the built-in unittest module.

Learn More β†’

Testing Assignments

Practice your testing skills with assignments.

Learn More β†’
Interview Resources
  • Python Syntax & Variables Interview Questions
  • Top SQL Interview Questions & Answers
  • SQL Joins: Displaying Data from Multiple Tables FAQ
  • Python Lists and Dictionaries Interview Questions
  • Python OOP Interview Questions
  • SQL Set Operators Interview Questions
Previous: Unit Testing Next: Mocking β†’