Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›pytest & Testing›Writing Your First Test

Getting Started

Writing Your First Test

This guide introduces the fundamental structure of creating test files and functions that execute with the pytest framework. Understanding these mechanics is essential for building a robust, automated verification suite for your codebase. You will reach for these concepts whenever you need to assert that your implementation logic behaves correctly under specific conditions.

The Anatomy of a Test Function

At the heart of any test suite lies the standard Python function, but with a specific naming convention that tells the test runner where to look. By prefixing your function names with 'test_', you explicitly signal to the framework that this logic represents a verification task rather than a utility or application component. When the runner executes, it performs a discovery process, scanning your project directory for these identifiers and collecting them into an execution queue. This design is intentionally minimalist, favoring convention over configuration. Because the test runner automatically inspects your module's namespace, you avoid the overhead of explicit registration or complex setup files. Understanding this discovery mechanism is crucial; it means that as long as your files follow the 'test_*.py' pattern and functions start with 'test_', the runner will integrate them seamlessly, ensuring that even as your application grows to hundreds of modules, your testing infrastructure remains automatically synchronized with your development work.

# File: test_math_utils.py

def test_addition_logic():
    # The function name MUST start with 'test_'
    # pytest discovers this automatically during collection
    result = 2 + 2
    assert result == 4  # The assertion verifies the condition

Leveraging Native Assertions

A common point of confusion for newcomers is the fear that they must learn a proprietary syntax to check outcomes. In reality, the framework leverages standard Python assertions. When you write an 'assert' statement, you are simply asking the environment to evaluate a boolean expression; if the expression is True, the test passes, and if it is False, the framework halts execution and reports a failure. The genius of this approach lies in the framework's ability to introspect the expression at runtime. When an assertion fails, the runner performs AST (Abstract Syntax Tree) transformation to break down the expression, revealing exactly which values contributed to the failure. This provides immediate, granular feedback on why your logic deviated from expectations. By relying on native language features, you maintain a cleaner codebase that is easily readable by anyone who knows the language, removing the mental barrier of learning specialized assertion libraries that are often required in other ecosystems.

# File: test_assertions.py

def test_string_formatting():
    user = "Alice"
    # Use standard assert statements
    # pytest provides detailed introspection on failure
    assert f"Hello, {user}" == "Hello, Alice"

Handling Expected Exceptions

Professional-grade software must handle failure states just as elegantly as success states. Simply verifying that a function returns the correct output is insufficient if the function is expected to raise an exception under invalid input conditions. To handle this, the framework provides a context manager designed to capture and validate errors. When you wrap code within this context manager, you are telling the runner: 'I expect this block to crash, and if it does not, treat that as a failure.' This is a critical pattern for robust design because it allows you to test error handling and edge cases without cluttering your tests with complex 'try/except' blocks. It ensures that your code is not just failing, but failing for the right reasons. By asserting the specific exception type, you ensure that future refactors don't accidentally swallow errors or change the interface in ways that break dependent client code.

# File: test_exceptions.py
import pytest

def divide(a, b):
    return a / b

def test_division_by_zero():
    # Verify that the correct exception is raised
    with pytest.raises(ZeroDivisionError):
        divide(10, 0)

Organizing Tests into Classes

As your test suite scales, simple flat functions may become difficult to organize or group logically. Grouping related tests within a class provides a structured way to namespace your test functions, making it easier to filter, run, or selectively ignore certain segments of your suite. A class also provides a shared environment where tests can share state or helper methods without polluting the global module namespace. When naming classes, the standard practice is to use the 'Test' prefix; the runner will treat these classes as containers for your test functions. This structure is particularly helpful when you want to group tests by feature or module, such as grouping all authentication-related tests inside a 'TestAuthentication' class. This organizational strategy helps maintain readability, as developers can navigate through the codebase by looking for these structured containers, significantly reducing the cognitive load required to find the relevant logic for a specific business requirement.

# File: test_classes.py

class TestUserAuthentication:
    # Methods in this class can be run as a group
    def test_login_success(self):
        assert True

    def test_login_failure(self):
        assert True

Isolating Test Logic with Modular Design

The goal of writing your first tests is to create a feedback loop that is both fast and isolated. Isolation ensures that the outcome of one test does not influence another, preventing 'spooky action at a distance' where a failure in one area appears as a failure in an unrelated section. By keeping tests small, focused, and free of side effects, you ensure that when a test turns red, you know exactly which line of production code is responsible. Avoid shared global variables or static state across different test functions. Instead, write your tests to verify a single unit of behavior. If you find your tests becoming too long, it is usually a sign that your production code is doing too much; use this as a feedback signal to refactor your production implementation into smaller, more modular functions. This synergy between testing and production architecture leads to code that is naturally decoupled and infinitely more maintainable over time.

# File: test_modular.py

def calculate_discount(price, rate):
    return price * (1 - rate)

def test_discount_calc():
    # Focused, isolated test verifying a single logic branch
    assert calculate_discount(100, 0.2) == 80.0

Key points

  • Test files and functions must begin with 'test_' to be automatically discovered by the runner.
  • Native Python 'assert' statements are used to verify results without requiring specialized library syntax.
  • The runner provides enhanced introspection, automatically showing the values involved in a failed assertion.
  • The 'pytest.raises' context manager is the standard way to verify that functions trigger expected exceptions.
  • Classes can be used to group related test functions, provided they also begin with the 'Test' prefix.
  • Maintaining test isolation is vital to ensure that a failure in one area does not leak into other unrelated tests.
  • The framework scans your directories recursively, allowing for flexible project structure and organization.
  • Writing focused tests often highlights areas of your production code that may benefit from modular refactoring.

Common mistakes

  • Mistake: Naming test files without the 'test_' prefix. Why it's wrong: pytest will not discover or execute these files. Fix: Ensure all test files follow the 'test_*.py' naming convention.
  • Mistake: Asserting results using standard Python 'assert' with a custom message. Why it's wrong: It limits pytest's ability to introspect and provide detailed assertion failure reports. Fix: Use simple 'assert condition' statements and let pytest handle the reporting.
  • Mistake: Over-reliance on setup/teardown functions instead of fixtures. Why it's wrong: Fixtures provide dependency injection and better reusability. Fix: Refactor setup logic into 'pytest.fixture' decorated functions.
  • Mistake: Using global variables to store state between tests. Why it's wrong: Tests become dependent on execution order and prone to side effects. Fix: Use fixtures to provide fresh, isolated state for each test.
  • Mistake: Writing large, monolithic test functions that cover multiple scenarios. Why it's wrong: It makes debugging difficult when a failure occurs. Fix: Follow the 'one assertion per concept' principle to keep tests focused.

Interview questions

What is the basic structure of a test function in pytest, and how does the framework discover it?

In pytest, a basic test is simply a function that starts with the prefix 'test_'. The framework automatically discovers these functions by scanning the directory for files that also start with 'test_' or end with '_test.py'. Inside the file, you write standard Python assertions to validate behavior. For example, 'def test_addition(): assert 1 + 1 == 2'. This simplicity is the core philosophy of pytest; it removes the need for complex boilerplate classes, allowing you to focus entirely on the logic you are testing.

Why is it important to use plain 'assert' statements in pytest rather than specialized methods like 'self.assertEqual'?

Using plain 'assert' statements is superior because pytest uses introspection to provide highly detailed failure reports. When an assertion fails, pytest rewrites the assertion to inspect the values of the expression, showing you exactly what went wrong without needing custom assertion methods. For example, if you write 'assert x == y', and they are not equal, pytest will display both the value of x and y in the console output. This makes debugging significantly faster compared to frameworks that require you to learn and memorize dozens of specialized assertion methods.

Can you explain how to group related tests together, and why we would organize them into classes?

You can group tests by placing them inside a class prefixed with 'Test', such as 'class TestCalculator'. While pytest doesn't require classes, they are useful for logical organization and grouping related tests together. Furthermore, classes allow you to use class-level fixtures for shared setup and teardown logic. While individual functions are often sufficient, classes help maintain a clean project structure when a single component has many different test scenarios that share common initialization requirements.

How would you compare the use of a simple setup function versus using pytest fixtures for managing test dependencies?

A simple setup function is often hardcoded and limited in scope, making it difficult to maintain as your test suite grows. In contrast, pytest fixtures provide a powerful, modular, and reusable way to handle dependencies. With fixtures, you can request specific data or states by name in your test parameters, and pytest handles the execution order and cleanup automatically. Fixtures support scoping—such as function, class, module, or session—which allows you to control the lifecycle of expensive resources like database connections far more effectively than manual setup and teardown functions ever could.

What is the purpose of the 'conftest.py' file in a pytest project, and how does it affect test discovery?

The 'conftest.py' file acts as a centralized configuration and fixture repository for your test suite. Any fixture defined in a 'conftest.py' file is automatically available to all tests in its directory and subdirectories without needing to import them explicitly. This is crucial for maintaining a DRY (Don't Repeat Yourself) codebase. Regarding discovery, pytest looks for 'conftest.py' files during the collection phase, ensuring that shared utility code or complex setup logic is loaded before any tests are executed, providing a clean and scalable way to manage project-wide testing requirements.

How does pytest handle test failures during the execution phase, and what strategies should you use to ensure robust testing?

When a test fails, pytest stops the execution of that specific function immediately, marks it as failed, and generates a report with the traceback and local variables. To ensure robustness, you should aim for atomic tests that do one thing, keeping them independent so that a failure in one does not cascade. Use fixtures for predictable environment setup and focus on clear, descriptive naming. Additionally, leveraging pytest's parametrization allows you to run the same logic against multiple input datasets, which significantly increases code coverage and helps identify edge cases that a single standard test might miss.

All pytest & Testing interview questions →

Check yourself

1. Which of the following describes the purpose of the 'test_' prefix in pytest?

  • A.It is a required syntax for the Python interpreter
  • B.It acts as a marker for the test discovery process
  • C.It optimizes the execution speed of the test suite
  • D.It forces the test to run with elevated privileges
Show answer

B. It acts as a marker for the test discovery process
The 'test_' prefix is the default pattern pytest uses to discover files and functions; without it, pytest ignores the code. The other options are incorrect as it is a configuration convention, not a language requirement or performance optimization.

2. When a simple 'assert' statement fails in pytest, what is the primary benefit of the reporting system?

  • A.It automatically fixes the bug in the source code
  • B.It outputs a full memory dump of the testing process
  • C.It provides a detailed breakdown of the values involved in the expression
  • D.It sends an email notification to the developers
Show answer

C. It provides a detailed breakdown of the values involved in the expression
Pytest uses introspection to rewrite assertions, providing detailed info on variables during failure. It does not auto-fix code, dump memory, or handle notifications, as those are outside the scope of assertion reporting.

3. Why is it recommended to use fixtures over standard setup/teardown methods?

  • A.Fixtures allow tests to run in parallel without conflict
  • B.Fixtures provide modularity, dependency injection, and scope management
  • C.Fixtures are required by the Python language specification
  • D.Fixtures are the only way to import external libraries
Show answer

B. Fixtures provide modularity, dependency injection, and scope management
Fixtures offer scope management (e.g., session vs function) and dependency injection, making tests cleaner. Parallelism is handled by plugins, not fixtures themselves, and fixtures are not language requirements or needed for library imports.

4. What happens if a test function fails during its execution in a suite of 10 tests?

  • A.The entire test suite halts immediately
  • B.Pytest logs the error and continues to run the remaining tests
  • C.The failing test is automatically deleted to prevent future noise
  • D.The remaining tests are skipped to ensure data integrity
Show answer

B. Pytest logs the error and continues to run the remaining tests
Pytest is designed to run all discovered tests regardless of individual failures, reporting results at the end. It does not halt the suite, delete tests, or skip others unless explicitly configured to do so via command-line flags.

5. In the context of writing your first test, what does 'test isolation' imply?

  • A.Tests must be located in their own dedicated directory
  • B.A test should not rely on the state or side effects of another test
  • C.Tests must be run in complete network isolation
  • D.Every test needs to use a unique mock object
Show answer

B. A test should not rely on the state or side effects of another test
Isolation ensures that tests are independent, making them predictable and debuggable. The other options are either false requirements (directory structure) or specific implementation details (network or mocks) that do not define the core concept of isolation.

Take the full pytest & Testing quiz →

← PreviousIntroduction to Testing and pytestNext →Running Tests and Reading Output

pytest & Testing

25 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app