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›Test Discovery and Naming Conventions

Getting Started

Test Discovery and Naming Conventions

Test discovery is the mechanism by which the framework automatically identifies and executes test code without requiring manual registration. Understanding these naming conventions allows developers to integrate new tests seamlessly into existing suites without complex configuration changes. You reach for these rules whenever you initialize a project or need to ensure your CI/CD pipelines correctly identify and execute your suite.

Standard File Discovery

The framework operates on a principle of convention over configuration, meaning it searches your filesystem for files that meet specific criteria to include them in the test suite. By default, the tool recursively traverses the directory tree looking for files prefixed with 'test_' or suffixed with '_test.py'. This predictable structure allows the runner to identify candidate modules rapidly while ignoring non-test source code. When the runner starts, it initiates a 'session', scanning these files to build a collection of test items. This architecture ensures that you do not need to maintain an explicit manifest of test files; as long as your naming follows the pattern, the framework will detect and execute them automatically. By strictly following this naming convention, you ensure that your testing suite remains organized and that future developers can easily distinguish test logic from production application modules without external documentation.

# File: test_calculations.py
# The runner will find this because it starts with 'test_'
def test_addition():
    assert 1 + 1 == 2

# File: math_utils_test.py
# The runner will find this because it ends with '_test.py'
def test_subtraction():
    assert 5 - 2 == 3

Function and Method Discovery

Once the framework has identified a valid test file, it performs an introspection of that file's namespace to find test functions. The internal discovery engine specifically looks for functions or class methods that are prefixed with 'test_'. This specific prefixing acts as a filter, allowing you to include helper functions, data processing utilities, or secondary classes within the same file without them being misinterpreted as actual test cases. The discovery mechanism is efficient because it avoids importing unnecessary overhead; it simply inspects the structure of the module. When it encounters a function matching the 'test_' pattern, it wraps that function into a test object, which becomes a unit of execution. This separation of concerns—identifying files, then identifying functions—provides a robust way to scale large suites while preventing unwanted execution of support code that shares the same file environment.

# File: test_user_validation.py

# The framework executes this because it starts with 'test_'
def test_email_format():
    assert "@" in "test@example.com"

# This helper function is IGNORED by the runner
# It lacks the 'test_' prefix, allowing it to be used by tests
def validate_internal_data(data):
    return True

def test_data_processing():
    assert validate_internal_data("some_data") is True

Class-Based Test Grouping

Grouping tests within classes offers a logical way to categorize related test cases, such as those targeting a specific model, service, or feature set. When using classes, the framework expects the class name to start with 'Test' (case-sensitive) and to contain no 'init' methods. If a class satisfies these constraints, every method inside that class that starts with 'test_' will be discovered and executed as an individual test instance. This hierarchical approach is particularly useful for managing shared state via setup and teardown fixtures, which can be applied to all tests within that class. The reasoning behind the 'Test' class prefix is to provide a visual cue to the framework's collector to look inside the scope of the class object. By organizing your suite into these logical containers, you maintain high readability and allow for granular control over how tests are grouped or excluded during a test run.

# File: test_api_integration.py

# The class name starts with 'Test'
class TestUserAuthentication:
    # Methods inside must also start with 'test_'
    def test_login_success(self):
        assert True

    def test_login_failure(self):
        assert False is False

# This class will NOT be collected as it lacks 'Test' prefix
class HelperUtilities:
    def test_internal_check(self):
        pass

Recursive Directory Traversal

The framework's discovery process is not limited to the root directory; it performs a deep, recursive walk of the entire filesystem starting from the current directory. This means that as long as your subdirectories contain valid 'test_*.py' or '*_test.py' files, the collector will find them. This capability is crucial for managing massive projects where tests are segregated into folders corresponding to specific application modules. The discovery algorithm ignores directories that do not contain valid test files, ensuring that the process remains fast and does not waste resources scanning vendor libraries or non-test code. Because the discovery is hierarchical, you can limit the scope of the tests run by providing a specific directory path to the command-line interface. This recursive design allows for a natural mapping between your application architecture and your test suite layout, which significantly simplifies the maintenance of large codebases by keeping tests colocated with their relevant source code.

# Directory Structure:
# /app
#   /auth
#     test_auth.py (Discovered)
#   /payments
#     test_payments.py (Discovered)

# Running command from root:
# pytest
# The runner will traverse both /auth and /payments

Handling Discovery Failures

Understanding why a test might not be discovered is vital for effective troubleshooting. The most common cause of failure is an incorrect file prefix or a missing 'test_' prefix on functions and classes. Furthermore, if you implement a class that contains an '__init__' method, the discovery engine will skip that class entirely because it assumes the class is not intended as a test collection. Another factor involves import errors; if a test file contains a syntax error or a broken dependency at the top level, the discovery process might stop or exclude that specific file entirely. Always remember that the framework needs to import the test file to inspect its contents. Therefore, if your file cannot be imported, it cannot be discovered. By verifying your naming conventions and ensuring your test files are importable, you remove the guesswork and ensure every test is accounted for during the execution cycle.

# This file will be ignored due to the __init__ method
class TestDatabase:
    def __init__(self):
        self.db = "connect"

    def test_connection(self):
        assert self.db == "connect"

# This file will be discovered correctly
class TestService:
    def test_service_is_active(self):
        assert True

Key points

  • Files must start with 'test_' or end with '_test.py' to be discovered by the framework.
  • Functions and methods must be prefixed with 'test_' to be identified as individual test cases.
  • Test classes must have a name starting with 'Test' and should not include an '__init__' method.
  • The runner performs a recursive scan of the filesystem starting from the location where the command is executed.
  • Discovery involves the framework importing modules to inspect their structure, which means import errors prevent discovery.
  • Categorizing tests into 'Test' classes allows for better organization and the use of setup/teardown fixtures.
  • Prefix conventions act as a filter to allow non-test utility code to reside in the same file without being executed.
  • If a test function or class is not being picked up, double-check that the file and the object names strictly follow the required prefixes.

Common mistakes

  • Mistake: Naming test files without the 'test_' prefix. Why it's wrong: pytest will not automatically discover these files. Fix: Rename files to 'test_*.py' or '*_test.py'.
  • Mistake: Defining test functions without the 'test_' prefix. Why it's wrong: pytest ignores functions not starting with 'test_'. Fix: Rename functions to start with 'test_'.
  • Mistake: Including an '__init__.py' file in the tests directory. Why it's wrong: It can cause issues with namespace packages and import confusion. Fix: Remove '__init__.py' from the tests directory.
  • Mistake: Assuming tests run in the order they are defined. Why it's wrong: pytest does not guarantee execution order. Fix: Design tests to be independent and isolated.
  • Mistake: Placing setup/teardown logic outside of fixtures. Why it's wrong: It lacks the modularity and lifecycle management of pytest fixtures. Fix: Use '@pytest.fixture' for setup/teardown tasks.

Interview questions

How does pytest decide which files and functions are considered tests?

Pytest uses a specific collection process based on naming conventions to identify tests automatically. By default, it looks for files that start with 'test_' or end with '_test.py'. Within those files, it identifies functions prefixed with 'test_' and classes that start with 'Test' and do not have an '__init__' method. This is a powerful feature because it minimizes configuration, allowing developers to simply name their files and functions correctly for them to be discovered and executed immediately.

Why is it important to follow the standard pytest naming conventions instead of just using custom naming?

Adhering to standard naming conventions is crucial for maintaining project consistency and ensuring seamless developer onboarding. When a new developer joins a project, they intuitively know how to run tests because pytest relies on the default 'test_' prefixes. If you use custom naming, you have to modify the configuration files like 'pytest.ini' or 'pyproject.toml'. Customizing reduces portability and makes it harder for automated tools, IDEs, and other developers to understand where the tests live without inspecting your complex configuration.

How can you run a specific set of tests using command-line arguments based on names?

You can use the '-k' flag, which allows for expression-based filtering of test names. For example, running 'pytest -k "auth or login"' will execute all tests containing the strings 'auth' or 'login' in their function or class names. This is incredibly efficient when you are working on a specific feature and want to run only a subset of the test suite without waiting for the entire project to execute, thereby speeding up the immediate feedback loop during active development.

Compare using the 'pytest.ini' configuration file versus the 'pyproject.toml' file for defining test discovery rules.

The 'pytest.ini' file has been the traditional way to configure pytest, but 'pyproject.toml' is the modern standard endorsed by the Python community for packaging and configuration. Using 'pyproject.toml' centralizes your configuration in a single file, which simplifies project structure. While 'pytest.ini' is still functional and widely supported, 'pyproject.toml' allows for better integration with other tools in the ecosystem. I recommend 'pyproject.toml' for new projects because it is more future-proof and keeps the project root clean from excessive configuration files.

What happens if a test class includes an '__init__' method, and how does this affect discovery?

In pytest, adding an '__init__' method to a test class is generally considered an anti-pattern and often causes the test collector to skip that class entirely. Pytest instantiates test classes to run their methods, and a custom '__init__' can interfere with the framework's internal setup process. Instead of using '__init__', you should use the 'pytest.fixture' mechanism or the 'setup_method' and 'teardown_method' hooks to manage state. By avoiding '__init__', you ensure that pytest can correctly instantiate your class and run your test cases without encountering unexpected initialization errors.

How would you handle a scenario where you have a large project with thousands of tests that follow different naming conventions than the pytest default?

To handle non-standard naming in a large project, I would define these patterns in the 'python_files', 'python_classes', and 'python_functions' configuration options within 'pyproject.toml'. For instance, I would add 'check_*.py' to 'python_files'. However, I would approach this with caution. While this allows us to pick up legacy code, it makes the test suite harder to read for outsiders. The best practice is to slowly refactor the test names to follow the standard 'test_' convention, as this aligns with the community expectation, simplifies CI/CD pipelines, and avoids complex, hidden configuration that is difficult to debug when new team members attempt to run the suite.

All pytest & Testing interview questions →

Check yourself

1. Which file naming pattern does pytest use by default for test discovery?

  • A.spec_*.py
  • B.test_*.py or *_test.py
  • C.tests_*.py
  • D.case_*.py
Show answer

B. test_*.py or *_test.py
pytest uses 'test_*.py' and '*_test.py' to identify test modules. Other options like 'spec_' or 'case_' are not standard pytest conventions and would require manual configuration.

2. If you have a function named 'calculate_sum' inside a test file, what will happen?

  • A.pytest will run it as a test automatically
  • B.pytest will error out during collection
  • C.pytest will ignore the function during execution
  • D.pytest will treat it as a fixture
Show answer

C. pytest will ignore the function during execution
pytest only discovers functions prefixed with 'test_'. Since 'calculate_sum' lacks this prefix, it is treated as a standard helper function and ignored by the test runner.

3. Why is it important to follow consistent naming conventions in a test suite?

  • A.It improves performance by reducing collection time
  • B.It allows pytest to discover and execute tests automatically
  • C.It is required by the Python language interpreter
  • D.It prevents naming collisions with production code
Show answer

B. It allows pytest to discover and execute tests automatically
Naming conventions allow pytest's discovery mechanism to locate tests without explicit configuration. Performance, language rules, and collision prevention are not the primary reasons for these specific naming patterns.

4. How does pytest handle test classes that do not start with 'Test'?

  • A.It runs them as regular test suites
  • B.It treats them as fixtures
  • C.It skips them by default during collection
  • D.It raises a warning but executes them anyway
Show answer

C. It skips them by default during collection
pytest skips classes that do not start with 'Test' by default, as they do not meet the collection criteria. It will not treat them as fixtures nor will it execute them.

5. When should you use a custom pytest configuration for discovery?

  • A.When you prefer using 'check_' as a prefix instead of 'test_'
  • B.When your project has thousands of tests
  • C.When you want to run tests in a specific folder
  • D.When you are using multiple testing frameworks
Show answer

A. When you prefer using 'check_' as a prefix instead of 'test_'
Custom configuration is specifically for changing the default discovery rules, such as supporting 'check_' prefixes. Project size, folder location, and multiple frameworks do not necessitate changing discovery patterns.

Take the full pytest & Testing quiz →

← PreviousRunning Tests and Reading OutputNext →Fixtures — Setup and Teardown

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