Organize and Run Python Tests

Learn how to structure test directories, run tests with unittest and pytest, configure test discovery, and integrate tests into your workflow.

7 min read

A test file that works when you run it directly is a good start. A test suite that runs with a single command from the project root, finds every test automatically, and finishes in seconds is what keeps you writing tests over the long term.

This article covers the standard directory layouts, the commands that run your whole test suite, and the configuration files that customize how tests are discovered. It assumes you are comfortable writing tests with either Python unittest or pytest.

The standard test directory layout

The Python community has converged on a single recommended layout: a top-level tests directory that mirrors the structure of your source code. If your project looks like the left side below, your test directory should mirror it as shown on the right.

texttext
Source layout:              Test layout:
my_project/                 my_project/
    calculator.py               tests/
    utils/                          test_calculator.py
        strings.py                  utils/
        numbers.py                      test_strings.py
                                        test_numbers.py

Each source module has a corresponding test file with a test_ prefix. The directory structure is identical, so the location of a test for utils/strings.py is always tests/utils/test_strings.py. This consistency matters when your project grows to dozens of modules.

Include empty init.py files in each test subdirectory for unittest discovery compatibility across Python versions.

Running tests with unittest discovery

The unittest module can discover and run every test in your project without you listing files manually. From your project root, run:

bashbash
python -m unittest discover

This command starts in the current directory, recurses into subdirectories, and runs every file matching test*.py. The shortcut python -m unittest does the same thing.

Customize discovery with three options:

  • -s sets the start directory.
  • -p changes the file pattern.
  • -t specifies the top-level directory.
bashbash
python -m unittest discover -s tests -p "*_test.py" -t .

Change the pattern when your naming convention differs from the default, or when you want to run only integration tests stored in files named *_integration_test.py.

Running tests with pytest

pytest's test discovery works similarly. From the project root, run:

bashbash
pytest

pytest finds files matching test_*.py or *test.py, functions starting with test, and classes starting with Test. It does not require init.py files for discovery.

Common pytest command-line options:

bashbash
pytest -v                    # Verbose: show every test name
pytest -x                    # Stop after the first failure
pytest -k "string"           # Run only tests whose name contains "string"
pytest tests/utils/          # Run only tests in a specific directory
pytest --lf                  # Run only tests that failed last time
pytest --ff                  # Run failures first, then the rest

The -k flag is especially useful during development. When you are working on the string utilities module, run pytest -k "string" to execute only the relevant tests. The feedback loop shrinks to a fraction of a second.

Because pytest does not require init.py files, you can add a tests folder to an existing project without restructuring anything else. Drop a test_*.py file anywhere under the project root and pytest finds it on the next run.

Configuring pytest

pytest reads configuration from pyproject.toml, pytest.ini, or setup.cfg. A pyproject.toml file in your project root is the modern standard:

tomltoml
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = ["-v", "--tb=short"]
markers = [
    "slow: tests that take longer than 1 second",
    "smoke: critical tests that must pass before merge",
]

The testpaths option tells pytest to search only the tests directory, avoiding scans of virtual environments or build artifacts. The addopts option sets default flags so you do not need to type -v every time. The markers section registers custom markers to avoid warnings about unknown markers.

If more than one of these files exists in a project, pytest reads settings from only one of them and does not merge values across files. Keep configuration in a single file per project to avoid confusion about which settings actually apply.

What a complete test run looks like

A clean test run answers two questions: how many tests ran, and did any fail. Here is typical output:

texttext
============================= test session starts ==============================
platform darwin -- Python 3.13.2, pytest-8.3.4
rootdir: /Users/name/projects/my_project
collected 12 items
 
tests/test_calculator.py ....                                            [ 33%]
tests/utils/test_strings.py ......                                       [ 83%]
tests/utils/test_numbers.py ..                                           [100%]
 
============================== 12 passed in 0.08s ==============================

Twelve tests, all passing, under a tenth of a second. This is the target speed for a unit test suite.

pytest also sets its process exit code based on the result: 0 when every test passes, 1 when at least one test fails. A CI pipeline uses that exit code to decide whether the build should stop, so you rarely need to parse the text output yourself.

If your tests take longer than ten seconds, look for tests that sleep, hit the network, or open real files on disk. Those are integration tests and belong in a separate suite.

Separating unit and integration tests

Tests that require a database, an API call, or a large file operation are valuable but should not slow down your primary suite. Separate them by directory:

texttext
my_project/
    tests/
        unit/
            test_calculator.py
        integration/
            test_database.py

Run unit tests during development with pytest tests/unit. Run integration tests less frequently with pytest tests/integration. Alternatively, use markers to tag integration tests:

pythonpython
@pytest.mark.integration
def test_database_write():
    db = connect_to_test_db()
    db.insert({"name": "test"})
    assert db.count() == 1

Then run selectively with pytest -m "not integration" for daily use or pytest -m integration before a commit.

Tests in your daily workflow

The goal of organizing tests is not to satisfy a project template. It is to make running tests so easy that you do it without thinking.

After setting up your directory structure, your workflow reduces to this: save a file, run pytest in the terminal, read the result. If the tests pass, move on. If a test fails, fix the code and run again.

Both VS Code and PyCharm can discover and run tests from within the editor, showing pass and fail indicators next to each test function. The article on debugging Python programs in VS Code covers how to run and debug tests directly in the editor.

The next article in this section covers testing exceptions and edge cases, which will help you expand your test suite beyond the happy path.

Rune AI

Rune AI

Key Insights

  • Place tests in a top-level tests directory that mirrors the structure of your source code for predictable discoverability.
  • Add an empty init.py in each test subdirectory so unittest discovery can import test modules correctly.
  • Run the full suite with python -m unittest discover or pytest from the project root; both will find tests automatically.
  • Use pytest.ini or pyproject.toml to configure test discovery paths, custom markers, and default options.
  • Aim for a test suite that runs in under ten seconds so you can run it after every save without breaking your flow.
RunePowered by Rune AI

Frequently Asked Questions

Should I put my tests in the same directory as my source code or in a separate tests folder?

A separate tests folder at the project root is the community standard. It keeps production code clean, makes it easy to exclude tests from deployment packages, and allows test discovery to find all tests without scanning non-test directories. The tests folder should mirror the structure of your source code so the location of each test file is predictable.

How do I configure pytest to find tests in a non-standard directory?

Create a pytest.ini or pyproject.toml file at the project root and set the testpaths option. For example, testpaths = tests in pytest.ini tells pytest to look for tests only in the tests directory. You can also set testpaths to a list like tests/unit,tests/integration to search multiple directories.

What is the difference between python -m unittest and pytest for running tests?

Both discover and run tests, but pytest provides richer output by default, including colored diffs for assertion failures and a summary of slowest tests. pytest also supports plugins, parametrized fixtures, and custom markers for filtering. Use unittest if you want zero external dependencies. Use pytest if you want better output and a larger ecosystem of plugins and features.

Conclusion

How you organize and run your tests shapes how often you actually run them. A clear directory structure, a single command to execute the entire suite, and a fast feedback loop make testing a habit rather than a chore. Set up your project so that running tests takes one command and under ten seconds, and you will run them after every change.