Clarifying Python TDD Practices with pytest , Am I on the Right Track?

Clarifying Python TDD Practices with pytest – Am I on the Right Track? I’m trying to better understand Python TDD (Test-Driven Development) principles while building my project using pytest.

So far, writing tests early has helped me reason about code behavior more pragmatically. However, in my current workflow, all functionality was written before the tests except for 3 class variables. Technically, does this mean I’m not really following TDD?

I’m particularly curious about a few things related to Python TDD:

  • Should I be writing full tests for a method before implementing any of its logic?
  • Is the goal to ensure all tests pass regardless of test data changes (as long as functionality remains intact)?
  • Should the number of tests scale significantly with the complexity/size of the class?

I’ve included my current pytest-based test suite for reference (below).

Code for your reference

import pytest
import os
import sys

path = os.path.dirname(os.path.abspath(__file__))
path = path.replace("\\pytest", "")
sys.path.append(path)
path += "\\pyffi"
sys.path.append(path)

from NifExplorer import NifExplorer
from NifExplorer import NifFormat

@pytest.fixture(autouse=True, scope='session')
def setup_nifExplorer():
    Explorers = []

    explorer = NifExplorer()
    explorer.SetBlockType(NifFormat.NiNode)
    explorer.SetResultPath("\\pytest\\results")
    explorer.SetSearchPath("\\pytest\\nif\\base")

    explorer2 = NifExplorer()
    explorer2.SetBlockType(NifFormat.ATextureRenderData)
    explorer2.SetResultPath("\\pytest\\results")
    explorer2.SetSearchPath("\\pytest\\nif\\base")

    explorer3 = NifExplorer()
    explorer3.SetBlockType("NiNode")
    explorer3.SetResultPath("\\pytest\\testResults")
    explorer3.SetSearchPath("\\pytest\\nif\\base")

    Explorers.append(explorer)
    Explorers.append(explorer2)
    Explorers.append(explorer3)


    return Explorers   

@pytest.mark.usefixtures("setup_nifExplorer")
class TestNifExplorer:
    def NifExlorer_BlockType_Is_Not_None(self, setup_nifExplorer):
        assert setup_nifExplorer.BlockType != None

    def NifExplorer_SearchPath_Is_Not_None(self, setup_nifExplorer):
        assert setup_nifExplorer.SearchPath != None

    def NifExplorer_ResultPath_Is_Not_None(self, setup_nifExlorer):
        assert setup_nifExlorer.ResultPath != None
        
    @pytest.mark.parametrize('funcs', (NifExplorer_SearchPath_Is_Not_None, NifExplorer_ResultPath_Is_Not_None, NifExlorer_BlockType_Is_Not_None))
    def test_NifExplorer_Variables_Equal_Not_None(self, setup_nifExplorer, funcs):
        for obj in setup_nifExplorer:
            funcs(self,obj)
        
    def NifExplorer_ResultPath_Directory_Exists(self, setup_nifExplorer):
        assert os.path.exists(setup_nifExplorer.ResultPath) == True

    def NifExplorer_SearchPath_Directory_Exists(self, setup_nifExplorer):
        assert os.path.exists(setup_nifExplorer.SearchPath) == True

    def NifExplorer_SearchPath_Directory_Contains_No_Forward_Slashes(self, setup_nifExplorer):
        assert setup_nifExplorer.SearchPath.count('/') < 1

    def NifExplorer_ResultPath_Directory_Contains_No_Forward_Slashes(self, setup_nifExplorer):
        assert setup_nifExplorer.ResultPath.count('/') < 1

    @pytest.mark.parametrize('funcs', [NifExplorer_ResultPath_Directory_Exists, NifExplorer_SearchPath_Directory_Exists, NifExplorer_SearchPath_Directory_Contains_No_Forward_Slashes, NifExplorer_ResultPath_Directory_Contains_No_Forward_Slashes])
    def test_NifExplorer_Directories_Exist_And_Paths_Contain_No_Forward_Slashes(self, setup_nifExplorer, funcs):
        for obj in setup_nifExplorer:
            funcs(self,obj)

    def NifExplorer_SearchPath_Contains_Nif_Files_Recursively(self, setup_nifExplorer):
        assert setup_nifExplorer.DirectoryContainsNifRecursively(setup_nifExplorer.SearchPath) == True

    @pytest.mark.parametrize('funcs', [NifExplorer_SearchPath_Contains_Nif_Files_Recursively])
    def test_NifExplorer_SearchPath_Contains_Nif_Files(self, setup_nifExplorer, funcs):
        for obj in setup_nifExplorer:
            funcs(self,obj)
if __name__ == "__main__":
    pytest.main()

Based on this, would you say I’m headed in the right direction with Python TDD, or am I misunderstanding something fundamental?

Would appreciate any feedback from those with more experience in this area!

Hey @anusha_gg

I noticed your test methods like NifExplorer_SearchPath_Is_Not_None aren’t prefixed with test_, so pytest won’t recognize them as standalone tests.

That’s why you need to parametrize funcs and manually call them inside test_NifExplorer_ methods.

If you’re following TDD, every behavior should be expressed as a test first, and the best way to make that work with pytest is to define each of them as a regular test_… function.

Here’s how you could restructure that:

def test_block_type_is_not_none(setup_nifExplorer):
    for obj in setup_nifExplorer:
        assert obj.BlockType is not None

This simplifies your parameterized logic, helps maintain atomic tests, and aligns better with TDD because you’re writing behavior-based assertions, one at a time.

Yes i agree too with @Priyadapanicker

However, You asked if writing code before the test breaks TDD. Technically yes , but it’s fixable.

For example, in your current code, the DirectoryContainsNifRecursively() method is being tested after it was already implemented.

A TDD move would be:

First, write this failing test:

def test_search_path_contains_nif_files(setup_nifExplorer):
    for obj in setup_nifExplorer:
        assert obj.DirectoryContainsNifRecursively(obj.SearchPath)

Then, implement the actual DirectoryContainsNifRecursively() logic just enough to make this test pass.

This workflow ensures that every behaviour is driven by a test and you never write unused or overengineered code. You can even stub the method initially with a return False to see the red → green flow in action.

Hope this helps :slight_smile:

One of your questions was whether tests should pass regardless of test data. The answer is: only if the expected behavior is still valid.

TDD tests are tied to behavior, not specific inputs.

In your case, hardcoded paths like \\pytest\\nif\\base might become brittle. If someone changes a directory name or adds a symbolic link, your tests might fail unexpectedly. You can mitigate this by either:

Using fixtures or mocks to simulate the file structure,

Or adding setup code to dynamically prepare test directories.

Example with tmp_path (pytest built-in):

def test_result_path_exists(tmp_path):
    result_dir = tmp_path / "results"
    result_dir.mkdir()
    assert result_dir.exists()

This ensures your tests aren’t tied to hardcoded filesystem states, which is vital when test data changes often.

Happy to help :slight_smile: