how to make a Python test driven development environment manually


requirements


preview

This is one way to make a Python Test Driven Development project. I walk through making the folders (directories) and files for the environment, including setting up the first test. By the end of the chapter you will know these commands better

mkdir
cd
touch
echo
cat
mv
rm
uv run pytest-watcher
source .venv/bin/activate
deactivate
history

questions about making a Python Test Driven Development Environment

Questions to think about as I go through the chapter


how to setup the project


how to setup a project with uv



how to change directory to the project


  • I try to change directory to the magic folder again

    cd magic
    

    the terminal shows

    .../pumping_python/magic
    

    uv made the directory for me

  • I use tree to see what uv added to the folder

    tree -a -L 2
    

    the terminal shows

    .
    ├── .git
       ├── HEAD
       ├── branches
       ├── config
       ├── description
       ├── hooks
       ├── info
       ├── objects
       └── refs
    ├── .gitignore
    ├── .python-version
    ├── README.md
    ├── main.py
    └── pyproject.toml
    

    it added a few files and folders.

    • the -L option tells tree how deep to go when showing the folders and files, I use 2 to make it show only the first level of contents of the child folders

    • Here is what uv made for me

      • .git this folder makes the project a git repository, it makes it easy to keep track of changes I make, and if I publish the repository I can work on the project from any computer anywhere

      • .gitignore is a file that tells git what files in the project to not keep track of, this is useful for things that I do not want or need to share

      • .python-version is a file that has the version of Python I am using, this helps if I do projects with different Python versions

      • README.md is a file that is used to describe the project

      • main.py is a Python module for me to write the code for the project

      • pyproject.toml is a file that is used to configure Python projects for packaging see pyproject.toml for more


how to see what is inside a file


  • I can use the cat program to look at what is inside .gitignore

    cat .gitignore
    

    the terminal shows

    # Python-generated files
    __pycache__/
    *.py[oc]
    build/
    dist/
    wheels/
    *.egg-info
    
    # Virtual environments
    .venv
    
  • I use cat to see what is inside pyproject.toml

    cat pyproject.toml
    

    the terminal and shows

    [project]
    name = "magic"
    version = "0.1.0"
    description = "Add your description here"
    readme = "README.md"
    requires-python = ">=3.XY"
    dependencies = []
    
  • I use cat to look at what is in .python-version

    cat .python-version
    

    the terminal is my friend, and shows

    3.XY
    
  • I use cat to show what is inside README.md

    cat README.md
    

    the terminal goes back to the command line

    .../pumping_python/magic
    

    the file is empty


how to run a Python program


I use Python to run the magic program

python3 src/magic.py

the terminal is my friend, and shows

python3: can't open file '.../pumping_python/magic/src/magic.py': [Errno 2] No such file or directory

Python cannot find the file because it does not exist, and there is no folder named src, yet.


how to make a directory for the source code


  • I make a child folder in the magic directory for the program because I want to keep the files separate from the other files in the project

    mkdir src
    

    the terminal goes back to the command line

  • I use tree to see what changed in the magic directory

    tree -a -L 1
    

    the terminal shows

    .
    ├── .git
       ├── HEAD
       ├── branches
       ├── config
       ├── description
       ├── hooks
       ├── info
       ├── objects
       └── refs
    ├── .gitignore
    ├── .python-version
    ├── README.md
    ├── main.py
    ├── pyproject.toml
    └── src
    
  • I try to run the magic program again

    python3 src/magic.py
    

    the terminal is my friend, and shows the same error from before because there is no file named magic.py in the src folder


how to change the name of a file


  • I use the mv program to change the name of main.py to magic.py and move it to the src folder

    mv main.py src/magic.py
    

    the terminal goes back to the command line

  • I use tree to see what folders and files I now have

    tree -a -L 2
    

    the terminal shows

    .
    ├── .git
       ├── HEAD
       ├── branches
       ├── config
       ├── description
       ├── hooks
       ├── info
       ├── objects
       └── refs
    ├── .gitignore
    ├── .python-version
    ├── README.md
    ├── pyproject.toml
    └── src
        └── magic.py
    

    main.py is now magic.py in the src folder

  • I try to run the magic program again

    python3 src/magic.py
    

    the terminal shows

    Hello from magic!
    

    Success! The uv Python package manager made the file with some Python code in it, I can successfully run it because the file is now in the folder.

  • I use cat to see what is in src/magic.py

    cat src/magic.py
    

    the terminal shows

    def main():
        print("Hello from magic!")
    
    
    if __name__ == "__main__":
        main()
    

    magic!


test_failure


how to manually run tests



how to make a directory for the tests


  • I make a child folder to keep the tests in a different place from the actual program

    mkdir tests
    

    the terminal goes back to the command line

  • I use tree to see what my project looks like

    tree -a -L 2
    

    the terminal is my friend, and shows

    .
    ├── .git
       ├── HEAD
       ├── branches
       ├── config
       ├── description
       ├── hooks
       ├── info
       ├── objects
       └── refs
    ├── .gitignore
    ├── .python-version
    ├── README.md
    ├── pyproject.toml
    ├── src
       └── magic.py
    └── tests
    

how to make a Python file for the tests in the ‘tests’ directory


  • I use touch to add an empty file to the tests directory for the actual test

    touch tests/magic.py
    

    the terminal goes back to the command line

  • I use tree to see what the project looks like now

    tree -a -L 2
    

    the terminal is my friend, and shows

    .
    ├── .git
       ├── HEAD
       ├── branches
       ├── config
       ├── description
       ├── hooks
       ├── info
       ├── objects
       └── refs
    ├── .gitignore
    ├── .python-version
    ├── README.md
    ├── pyproject.toml
    ├── src
       └── magic.py
    └── tests
        └── magic.py
    
  • I run the test again

    python3 -m unittest
    

    the terminal is my friend, and shows

    NO TESTS RAN
    

    because I have not set up the test correctly, yet


RED: make it fail


how to make the tests a Python package


  • I use touch to add an empty file with the name __init__.py to the tests folder

    Danger

    use 2 underscores (__) before and after init for __init__.py not _init_.py

    touch tests/__init__.py
    

    the terminal goes back to the command line

  • I run the tree command to see what changed

    tree -a -L 2
    

    the terminal is my friend, and shows

    .
    ├── .git
       ├── HEAD
       ├── branches
       ├── config
       ├── description
       ├── hooks
       ├── info
       ├── objects
       └── refs
    ├── .gitignore
    ├── .python-version
    ├── README.md
    ├── pyproject.toml
    ├── src
       └── magic.py
    └── tests
        ├── __init__.py
        └── magic.py
    
  • I try to run the tests again

    python3 -m unittest
    

    the terminal does not feel like my friend, and shows

    NO TESTS RAN
    

    because unittest does not know that magic.py in the tests folder is a test file. I did not start the name with test_. I have to change the name

  • I close magic.py in the editor of the Integrated Development Environment (IDE)

    Danger

    if you do not close magic.py in the editor, there will be 3 files in the tests folder after the next step (instead of 2), because the Auto Save feature (enabled earlier) will save the original file if it is still open in the editor after you change its name

  • I use the mv program to change the name of magic.py in the tests folder to test_magic.py

    mv tests/magic.py tests/test_magic.py
    

    the terminal goes back to the command line

  • I use tree with the -L option to see what I have so far

    tree -a -L 2
    

    the terminal is my friend, and shows

    .
    ├── .git
       ├── HEAD
       ├── branches
       ├── config
       ├── description
       ├── hooks
       ├── info
       ├── objects
       └── refs
    ├── .gitignore
    ├── .python-version
    ├── README.md
    ├── pyproject.toml
    ├── src
       └── magic.py
    └── tests
        ├── __init__.py
        ├── __pycache__
        └── test_magic.py
    

    if you do not see __pycache__ in the tree do not worry,

    the important thing is that you renamed magic.py to test_magic.py for unittest to find the test

  • I run the tests again

    python3 -m unittest
    

    the terminal is my friend, and shows AssertionError

    F
    =========================================================
    FAIL: test_failure (tests.test_magic.TestMagic.test_failure)
    ---------------------------------------------------------
    Traceback (most recent call last):
      File "...pumping_python/magic/tests/test_magic.py", line 7, in test_failure
        self.assertFalse(True)
        ~~~~~~~~~~~~~~~~^^^^^^
    AssertionError: True is not false
    
    ---------------------------------------------------------
    Ran 1 test in A.XYZs
    
    FAILED (failures=1)
    

    Tip

    I can use any name for the test file. It must start with test_ or unittest will NOT run the tests in the file

    This is the RED part of the Test Driven Development Cycle. The message in the terminal is about the failure, I like to read these from the bottom up. Here is an explanation of each line, starting from the last line on the screen

    • FAILED (failures=1): the number of failures

    • Ran 1 test in A.XYZs: the number of tests it ran and how long they took

    • AssertionError: True is not false: the Error (Exception) that happened and its message, in this case AssertionError because True is NOT False

    • self.assertFalse(True): the line of code that caused AssertionError

    • ~~~~~~~~~~~~~~~~^^^^^^: points to the part of the line above, that Python thinks caused the error

    • File ".../magic/tests/test_magic.py", line 7, in test_failure: the line number of the code that caused the error and the location of the file where it is

    • Traceback (most recent call last):: all the information shown after this line that is indented to the right shows the calls that led to the failure. The last line is usually the most important one that points to what caused the failure, this is why I like to read it from the bottom up

    • FAIL: test_failure (tests.test_magic.TestMagic.test_failure) is a header with information in dot notation about the failing test method

      • tests.test_magic.TestMagic.test_failure is the location of the failing test

        • tests is the tests folder

        • test_magic is the test_magic.py file in the tests folder

        • TestMagic is the class defined on line 4 in test_magic.py in the tests folder

        • test_failure is the method (function) defined in the TestMagic class, on line 6 in test_magic.py in the tests folder

    • F shows a failure

  • I hold ctrl (Windows/Linux) or option/command (MacOS) on the keyboard and use the mouse to click on File ".../pumping_python/magic/tests/test_magic.py", line 7 in the terminal, and the Integrated Development Environment (IDE) opens the file in the editor with the cursor at the line where the failure happened


GREEN: make it pass

I change True to False on line 7 of test_magic.py in the editor

1import unittest
2
3
4class TestMagic(unittest.TestCase):
5
6    def test_failure(self):
7        self.assertFalse(False)

I run the test again in the terminal

python3 -m unittest

the test passes! the terminal is my friend, and shows

.
------------------------------------------------------
Ran 1 test in A.XYZs

OK

cue CELEBRATION MUSIC AND DANCE! I am GREEN!!


REFACTOR: make it better

I keep a list of Errors/Exceptions that show up in the terminal as I go through this book to help me know them better, it helps when I run into them later, because I will be more familiar with them. I add AssertionError to test_magic.py in the editor

 1import unittest
 2
 3
 4class TestMagic(unittest.TestCase):
 5
 6    def test_failure(self):
 7        self.assertFalse(True)
 8
 9
10# Exceptions seen
11# AssertionError

Note

comments in Python are written with # at the beginning, they do not do anything, they are notes for me

  • I ran python3 -m unittest to see the test fail

  • I ran python3 -m unittest every time I made a change until the test passed

  • I will run python3 -m unittest again when I add any code, to make sure tests that passed before do not fail and that the new code I add does what I want

This means I have to run python3 -m unittest for each part of the Test Driven Development Cycle or any time there is a code change.

I do not want to type python3 -m unittest again, I want the computer to do it for me.


how to run the tests automatically


I can use pytest-watcher to run tests automatically. It is a Python program that automatically runs pytest any time a Python file changes in the folder it is looking at, this means it will run the tests for me every time I make a change.

pytest is a Python package like unittest that is used for testing. It is not part of The Python Standard Library

I use uv to run pytest-watcher in the terminal

uv run pytest-watcher

the terminal is my friend, and shows

Using CPython 3.13.13
Creating virtual environment at: .venv
error: Failed to spawn: `pytest-watcher`
  Caused by: No such file or directory (os error 2)

because pytest-watcher is not installed on the computer. I can install it with the uv Python Package Manager.


how to write text to a file


I want to make a file where I list all the Python packages that my project needs as a way to document it and have uv install the programs listed in the file

  • I can write text to a file with the echo program, it shows whatever it is given as an argument, on the screen (standard output (stdout)) for example, if I type this in the terminal

    echo "pytest"
    

    it shows

    pytest
    
  • I can also use echo to add text to a file. I use it to make the requirements file with pytest as what is inside it

    echo "pytest" > requirements.txt
    
  • I add pytest-watcher to the requirements file as well

    echo "pytest-watcher" >> requirements.txt
    
    • >> is an operator that is used to send output from a program to the given file, it adds to what is in the file without writing over it

    • pytest-watcher is a Python program that automatically runs pytest when I change code in the project

  • I use tree to see what the project looks like now

    tree -a -L 2
    

    the terminal shows

    .
    ├── .git
       ├── HEAD
       ├── branches
       ├── config
       ├── description
       ├── hooks
       ├── info
       ├── objects
       └── refs
    ├── .gitignore
    ├── .python-version
    ├── README.md
    ├── pyproject.toml
    ├── requirements.txt
    ├── src
       └── magic.py
    └── tests
        ├── __init__.py
        ├── __pycache__
        └── test_magic.py
    

    requirements.txt is now in the magic folder

  • I use cat to make sure requirements.txt has pytest and pytest-watcher inside it

    cat requirements.txt
    

    the terminal shows

    pytest
    pytest-watcher
    

    life is good!


how to install Python packages with uv


  • I use uv to install pytest-watcher from the requirements file

    uv add --requirement requirements.txt
    
    • --requirement is an option that can be given to the add argument for Python packages in a given file

    • requirements.txt is the name of the given file. It helps to manage Python programs that are needed by the project. In this case I only have two programs. A project can have a big number of programs it needs and using one file with one command is easier than one command for each program

    the terminal is my friend, and shows setup and installation

    Resolved 9 packages in GHIms
    ░░░░░░░░░░░░░░░░░░░░ [0/7] Installing wheels...
    ...
    Installed 7 packages in JKLms
     + iniconfig==A.B.C
     + packaging==DE.F
     + pluggy==G.H.I
     + pygments==J.K.L
     + pytest==M.N.O
     + pytest-watcher==P.Q.R
     + watchdog==S.T.U
    
  • I run tree to see what changed in the project

    tree -a -L 1
    

    the terminal shows

    .
    ├── .git
    ├── .gitignore
    ├── .python-version
    ├── .venv
    ├── README.md
    ├── pyproject.toml
    ├── requirements.txt
    ├── src
    ├── tests
    └── uv.lock
    

    uv added 2 things

  • I use cat to show what is now in pyproject.toml

    cat pyproject.toml
    

    the terminal is my friend, and shows

    [project]
    name = "magic"
    version = "0.1.0"
    description = "Add your description here"
    readme = "README.md"
    requires-python = ">=3.XY"
    dependencies = [
        "pytest>=M.N.O",
        "pytest-watcher>=P.Q.R",
    ]
    

    it added pytest and pytest-watcher to the dependencies of the project


what is a virtual environment?


A virtual environment is a separate folder where I can install Python packages that my project needs. This helps me keep things that belong to the project in one place, separate from other things on the computer.

It means I can have a separate virtual environment for every project with only the programs that the project needs. I do not have to keep every program I have ever used for projects that do not need them.


how to activate a virtual environment



how to deactivate a virtual environment


  • I leave the virtual environment by typing deactivate in the terminal

    deactivate
    

    the terminal goes back to the command line, (magic) is no longer on the left side

    .../pumping_python/magic
    

how to run the tests automatically with uv and pytest-watcher


  • I try to run the tests again, this time with uv

    uv run pytest-watcher . --now
    

    the terminal is my friend, and shows results without going back to the command line

    pytest-watcher version X.Y.Z
    Runner command: pytest
    Waiting for file changes in .../pumping_python/magic
    =================== test session starts ====================
    platform ____ -- Python 3.A.B, pytest-C.D.E, pluggy-F.G.H
    rootdir: .../pumping_python/magic
    configfile: pyproject.toml
    collected 1 item
    
    tests/test_magic.py .                                 [100%]
    
    ==================== 1 passed in X.YZs =====================
    [pytest-watcher]
    Current runner args: []
    Press w to show menu
    

how to open the test file in the editor from the terminal


  • I hold ctrl (Windows/Linux) or option/command (MacOS) on the keyboard, then click on tests/test_magic.py to place the cursor in the editor of the Integrated Development Environment (IDE), then I change False to True on line 7

    1import unittest
    2
    3
    4class TestMagic(unittest.TestCase):
    5
    6    def test_failure(self):
    7        self.assertFalse(True)
    

    the terminal is my friend, and shows AssertionError

    ======================== FAILURES ==========================
    _________________ TestMagic.test_failure ___________________
    
    self = <tests.test_magic.TestMagic testMethod=test_failure>
    
        def test_failure(self):
    >       self.assertFalse(True)
    E       AssertionError: True is not false
    
    tests/test_magic.py:7: AssertionError
    =============== short test summary info ====================
    FAILED tests/test_magic.py::TestMagic::test_failure - AssertionError: True is not false
    =================== 1 failed in X.YZs ======================
    
  • I hold ctrl (Windows/Linux) or option/command (MacOS) on the keyboard, then click on tests/test_magic.py:7 to place the cursor in the editor of the Integrated Development Environment (IDE), then I change True back to False in test_magic.py

    7      self.assertFalse(False)
    

    the test passes. I can write the rest of the code for the project and get results back quickly because the tests run when I change the code


how to stop the automated tests


I go to the terminal and use q on the keyboard to stop the tests, the terminal goes back to the command line

.../pumping_python/magic

close the project


review


how to view all the commands typed in a terminal



How many questions can you answer after going through this chapter?


what is next?

You have seen me make a Test Driven Development environment for a project called magic on any Linux, MacOS and Windows_computers. Would you like to test AssertionError next?


rate pumping python

If this has been a 7 star experience for you, please CLICK HERE to leave a 5 star review of pumping python. It helps other people get into the book too