5 Best Practices For Organizing Tests (Simple And Scalable)

Picture this: you join a growing project with thousands of tests. Or your project has grown over time to hundreds of tests.

Different developers work on the codebase, each with various skills and styles, leading to inconsistent best practices.

Scattered test files, inconsistent naming conventions, overloaded fixtures. and conftest.py files, creating more confusion than clarity.

Debugging even a single test failure takes hours. It’s messy and overwhelming, leaving you wondering where to begin and how to refactor.

But it doesn’t have to be this way.

For this article, I’ve reviewed and studied some of the best practices for organizing tests for simplicity, scalability, and efficiency.

I’ll also share stuff I learned from my 9 years of professional experience as a Python developer in the industry.

We’ll tackle the challenges of disorganized tests head-on and show you how to create a future-proof testing strategy.

You’ll learn how to:

  • Avoid common pitfalls like overloaded test files and monolithic tests.
  • Leverage the testing pyramid to effectively balance unit, integration, and end-to-end tests.
  • Structure your tests to mirror application code and separate them cleanly in dedicated folders.
  • Maximize Pytest’s flexibility to organize fixtures, manage test data, and control scope with conftest.py.
  • How to organize views, templates, and URLs for specific frameworks like Django, ensuring best practices.

By the end of this article, you’ll have the tools and techniques to build a scalable, maintainable test suite that grows with your project and makes it quick and easy to understand and onboard new developers.

Let’s dive in!

Why Disorganized Tests Are An Issue?

Before we go into solutions, we must talk about the problem first.

Disorganized tests introduce chaos into your development workflow, making even simple tasks unnecessarily complex.

Overloading a single test file or module with too many tests results in slow feedback and harder debugging.

Inconsistent naming conventions create confusion and waste time as new developers struggle to locate specific tests.

Global fixtures, when overused, lead to test interdependence, causing unrelated tests to fail unexpectedly.

Worse yet, overloaded fixtures with too many responsibilities blur the line between setup and test logic, making tests fragile and difficult to maintain.

Monolithic tests — large, unwieldy tests that try to do too much — are especially hard to debug and mask poor code design and bad practices.

And then there are flaky tests — tests that sometimes pass and sometimes fail.

Ignoring or leaving these unresolved frustrates your team and erodes confidence in your test suite.

Without proper organization, your tests become a liability instead of an asset.

Let’s look at some best practices on how to tackle these and organize your tests for ease today and tomorrow.

Best Practice # 1 — Organizing Tests by Testing Pyramid

One of the easiest ways to get started organizing your tests is to think of it in terms of the Testing Pyramid.

Testing Pyramid

Without getting into too much detail here, it means you have a lot of unit tests, fewer service or integration tests, and the least end-to-end or functional tests.

These could also be UI tests using tools like Selenium, Playwright or Cypress.

UI tests are slow and heavy while unit tests are fast and lightweight. If you’re unfamiliar with the test pyramid concept I recommend you check out Martin Fowler’s article.

  • Unit Tests —Fast, isolated tests that validate small, discrete units of code like functions or classes.
  • Integration Tests — These ensure that different components of your application work together as expected, such as testing APIs or database interactions.
  • End-to-End Tests — These simulate real-world workflows and validate the behavior of the entire application from the user’s perspective.

There is no agreed-upon definition of each of these, but I encourage you to familiarize yourself with the various types of testing.

Grouping your tests by type immediately gives developers a high-level perspective and aids in deciding what you need to test and in which order.

A typical folder structure may look like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tests/                      
├── unit/
│ ├── __init__.py
│ ├── test_user.py
│ ├── test_order.py
│ ├── test_payment_service.py
│ └── test_notification_service.py

├── integration/
│ ├── __init__.py
│ ├── test_api_endpoints.py
│ ├── test_database_interactions.py
│ └── test_service_integration.py

├── e2e/
│ ├── __init__.py
│ ├── test_user_journey.py
│ ├── test_checkout_flow.py
│ └── test_admin_dashboard.py


├── conftest.py
└── pytest.ini

Pytest also gives you the option to mark your tests using the @pytest.mark.X decorator for example — @pytest.mark.unit, which is very helpful.

You can also run tests directly using the marker for example

1
$ pytest -m unit

Check out this article if you need a refresher on Pytest markers.

Best Practice #2: Test Structure Should Mirror Application Code

Another good practice in organizing tests is to mirror your application’s folder structure.

This is a simple yet powerful way to ensure clarity and maintainability.

When your tests align with your application’s modules and directories, navigating between the two becomes intuitive — saving time and reducing confusion.

For example, if your application has separate modules for models, services, and controllers, your test directory should reflect this structure.

Each test file focuses on the corresponding module, making it easier to locate and update tests when code changes.

This structure also helps new team members quickly understand how the test suite relates to the application, making new feature development and refactors easier.

Example Structure:
If your application code looks like this:

1
2
3
4
5
6
7
8
9
src/  
├── models/
│ ├── user.py
│ └── order.py
├── services/
│ ├── payment_service.py
│ └── notification_service.py
└── controllers/
└── order_controller.py

Your test folder should mirror it like this:

1
2
3
4
5
6
7
8
9
tests/  
├── models/
│ ├── test_user.py
│ └── test_order.py
├── services/
│ ├── test_payment_service.py
│ └── test_notification_service.py
└── controllers/
└── test_order_controller.py

Benefits:

  1. Improved Readability: Directly map tests to the corresponding application components.
  2. Easier Debugging: Quickly locate and fix tests related to a specific module.
  3. Scalability: As the application grows, the test structure naturally evolves alongside it.
  4. Collaboration: This makes it easier for developers and QA teams to align their work.

By mirroring your application’s structure, your test suite becomes an organized, intuitive resource rather than a tangled mess of files.

Best Practice #3 — How to Group or Organize Fixtures

Fixtures are reusable pieces of code that can be used in tests, to reduce boilerplate, improve efficiency, and handle setup and teardown.

Organizing fixtures effectively is key to maintaining a clean and scalable test suite.

Poorly managed fixtures can lead to unnecessary complexity, slow tests, and hard-to-track dependencies.

By grouping and scoping your fixtures thoughtfully, you can streamline your testing process and improve efficiency.

Here’s a quick run-through of Pytest fixture scopes. If you’re familiar with that then please skip ahead.

Pytest fixtures can have different scopes, determining their lifespan and reuse:

Function-Scoped (default): The fixture is created and destroyed for each test function. Use this for test-specific setups, such as temporary files or isolated database states.

1
2
3
4
@pytest.fixture  
def temp_file():
with open("temp.txt", "w") as f:
yield f

Class-Scoped: The fixture is created once per test class and shared across its methods. Ideal for initializing objects or states used in all tests within a class.

1
2
3
@pytest.fixture(scope="class")  
def sample_data():
return {"key": "value"}

Module-Scoped: The fixture is shared across all tests in a module. Best for expensive setups like database connections or API clients.

1
2
3
@pytest.fixture(scope="module")  
def db_connection():
return connect_to_db()

There is also session scope which applies a fixture to an entire test session.

Centralized vs. Localized Fixtures: Finding the Right Balance

When managing fixtures in Pytest, the choice between a centralized fixtures folder and localized conftest.py files often depends on the size and complexity of your project.

Both approaches have unique benefits, and combining them into a hybrid strategy can offer the best of both worlds.

Centralized Fixtures

Using a dedicated fixtures/ folder is ideal for shared resources that multiple test directories rely on, such as database connections or API clients.

Grouping fixtures into modules like fixtures_db.py or fixtures_api.py keeps them organized and promotes reusability.

1
2
3
4
5
6
7
8
9
10
11
tests/  
├── fixtures/
│ ├── fixtures_db.py
│ ├── fixtures_api.py
│ └── fixtures_auth.py
├── unit/
│ ├── conftest.py
│ └── test_models.py
├── integration/
│ ├── conftest.py
│ └── test_api.py

But these come with tradeoffs.

Pros:

  • Centralized Management: Grouping fixtures by functionality (e.g., fixtures_db.py, fixtures_api.py) keeps them organized and easy to locate (especially mocks).
  • Reusability: Shared fixtures are available across your test suite without duplication.
  • Scalability: Works well for large projects with many test files and fixtures.
  • Better Separation of Concerns: Fixtures can be split into modules, reducing the size of any single file.

Cons:

  • Requires Explicit Imports: You must import fixtures into tests, which can make them less “magically available.”
  • Potential for Overuse: Centralized fixtures can lead to over-reliance, creating hidden dependencies between unrelated tests.

Using conftest.py in Local Directories (Localised Fixtures)

On the other hand, conftest.py files in specific test directories are perfect for test-specific setups.

These ensure that narrowly scoped logic doesn’t clutter the global namespace or affect unrelated tests.

For example.

1
2
3
4
5
6
7
tests/  
├── unit/
│ ├── conftest.py # Fixtures specific to unit tests
│ └── test_models.py
├── integration/
│ ├── conftest.py # Fixtures specific to integration tests
│ └── test_api.py

Pros:

  • Implicit Discovery: Pytest automatically discovers fixtures defined in conftest.py, so there’s no need to import them manually.
  • Localized Scope: Fixtures are scoped to a specific directory, reducing the risk of unintended dependencies between unrelated tests.
  • Simpler for Small Projects: Keeps fixtures tied closely to the tests they support, making it easy to understand their usage.

Cons:

  • Difficult to Scale: As the number of tests grows, managing fixtures across multiple conftest.py files can become unwieldy.
  • Duplication: Without careful planning, fixtures may be duplicated across directories, leading to maintenance challenges.

Best of Both Worlds

In larger projects, you may want to consider a hybrid approach:

  1. Use a fixtures/ folder for broadly shared, reusable fixtures like database connections or mocks.
  2. Use local conftest.py files for directory-specific or narrowly scoped fixtures, such as test-specific data or setup logic.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    tests/  
    ├── fixtures/
    │ ├── fixtures_db.py
    │ ├── fixtures_api.py
    │ └── fixtures_auth.py
    ├── unit/
    │ ├── conftest.py
    │ └── test_models.py
    ├── integration/
    │ ├── conftest.py
    │ └── test_api.py
    This approach keeps your test suite maintainable and scalable.

I like to keep my fixtures within a conftest.py file local to that directory and share common fixtures in a conftest.py file placed in the tests directory.

Having learned how to organize your fixtures, let’s see if it’s a good idea to include tests within the application code. And what factors drive that decision.

Best Practice #4 — Tests Outside vs. Inside Application Code

A key decision is whether to keep tests outside or inside (with) the application code.

Each approach has its advantages and trade-offs, and the right choice depends on your project’s size, purpose, and deployment strategy.

Tests Outside Application Code

Advantages:

  • Separation of Concerns: Keeping tests in a tests/ folder ensures your application code is clean and free of testing artifacts.
  • Clarity and Organization: A dedicated folder allows for a clear hierarchy and separation of unit, integration, and end-to-end tests.
  • Avoids Deployment Risks: Excluding tests from production deployments reduces overhead and mitigates potential security risks.
  • Scalability: Works well for large projects with extensive test suites.
1
2
3
4
5
6
7
8
9
project/  
├── src/
│ ├── models/
│ └── services/
├── tests/
│ ├── unit/
│ ├── integration/
│ ├── e2e/
│ └── fixtures/

Best suited for production-grade applications, where maintaining a clean separation between test code and production code is essential.

Tests as Part of Application Code

Advantages:

  • Tightly Coupled Testing: Embedding tests within application modules can simplify testing for libraries or reusable modules, as tests are colocated with the code they validate.
  • Smaller Projects: Useful for smaller projects or packages where the simplicity of having everything in one place outweighs the benefits of separation.
  • Quick Context: Developers can quickly access and understand the tests alongside the code.

Potential Risks:

  • Deployment Risks: Without proper filtering, test code might inadvertently be included in production builds.
  • Codebase Clutter: Embedding tests can make the codebase harder to navigate, especially as the project grows.
1
2
3
4
5
6
7
8
9
src/  
├── models/
│ ├── user.py
│ └── tests/
│ └── test_user.py
├── services/
│ ├── payment_service.py
│ └── tests/
│ └── test_payment_service.py

Ensure production builds exclude tests/ folders by configuring your deployment pipeline or packaging tools.

Choosing the Right Approach

When to Keep Tests Outside:

  • Large applications with multiple developers and complex testing needs.
  • Projects where deployment risks or production performance are critical.
  • Applications requiring robust test suites with a clear separation of concerns.

When to Embed Tests Inside:

  • Small libraries or standalone modules intended for reuse.
  • Quick prototyping or proof-of-concept projects.
  • When ease of access and simplicity are prioritized over scalability.

Best Practice #5 — Organizing Tests for Django Applications

Django applications often involve multiple components like models, views, templates, and serializers, where each one needs to be thoroughly tested.

Some ideas include grouping Django tests by views , models , templates and so on.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
tests/  
├── unit/
│ ├── models/
│ │ ├── test_user_model.py
│ │ ├── test_order_model.py
│ │ └── __init__.py
│ ├── serializers/
│ │ ├── test_user_serializer.py
│ │ └── test_order_serializer.py
│ ├── views/
│ │ ├── test_user_views.py
│ │ └── __init__.py
│ ├── templates/
│ │ └── test_user_templates.py
│ └── __init__.py

├── integration/
│ ├── test_user_workflow.py
│ ├── test_order_workflow.py
│ └── __init__.py

├── e2e/
│ ├── test_full_user_journey.py
│ ├── test_checkout_process.py
│ └── __init__.py

├── fixtures/
│ ├── fixtures_db.py
│ ├── fixtures_auth.py
│ └── __init__.py

└── conftest.py

These are just ideas and not hard and fast rules. Check out some sample Django projects for more ideas.

Some companies prefer to use Django Unit Tests and group their tests differently.

The best practice is the one that is easy for you and your team to understand, manage, and scale.

Conclusion

In this article, you learned how to transform your test suite from a chaotic mess into a scalable, maintainable asset.

We covered key strategies for organizing your tests effectively like grouping by test pyramid, mirroring application code and organizing fixtures thoughtfully.

You also learned how to decide whether to package tests with application code or keep them separate and lastly, how to group tests in a Django application.

The overarching message is clear: there’s no one-size-fits-all solution.

The most important step is to take the time to think critically about your test structure.

Aim to make it easy to understand, maintain, and scale. Don’t be afraid to adapt your approach as your project evolves.

Now it’s your turn to put these principles into practice.

Start small — refactor a few files, organize your fixtures, or implement the Testing Pyramid. See what works for you and your team, and iterate from there.

If you have ideas, questions, or strategies, I’d love to hear them.

Share your thoughts via email, and let’s continue to improve how we build robust applications!

Additional Reading