Import Error when using pytest

Hello, I’m trying to use pytest to test a module, but it keeps giving me an import error:
ImportError while importing test module ‘/home/roberto-padilla/projects/ex47/tests/ex47_test.py’.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.12/importlib/init.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
tests/ex47_test.py:2: in
from ex47.game import Room
E ModuleNotFoundError: No module named ‘ex47.game’

Here is my directory structure:
~/projects/ex47
bin/
docs/
ex47/
| init.py
| pycache/
│ └── init.cpython-312.pyc
game.py
pycache/
│ └── game.cpython-312.pyc
setup.py
tests/
| ex47_test.py
| pycache/
| ├── ex47_test.cpython-312-pytest-8.3.5.pyc
| └── init.cpython-312.pyc

Also, here is the code for ex47_test.py that asks to import game.py:

from pytest import *
from ex47.game import Room


def test_room():
   gold = Room("GoldRoom",
               """This room has gold in it you can grab.  There's a
               door to the north.""")
   assert gold.name, "GoldRoom"
   assert gold.paths, {}
   
def test_room_paths():
    center = Room("Center", "Test room in the center.")
    north = Room("North", "Test room in the north.")
    south = Room("South", "Test room in the south.")

    center.add_paths({'north': north, 'south': south})
    assert center.go('north'), north
    assert center.go('south'), south

def test_map():
    start = Room("Start", "You can go west and down a hole.")
    west = Room("Trees", "There are trees here, you can go east.")
    down = Room("Dungeon", "It's dark down here, you can go up.")

    start.add_paths({'west': west, 'down': down})
    west.add_paths({'east': start})
    down.add_paths({'up': start})

    assert start.go('west'), west
    assert start.go('west').go('east'), start
    assert start.go('down').go('up'), start 

Please help me figure out this error! Thank you!

Could you post this in code format? It looks game.py is outside of ex47 folder but should be inside.

Here you go!

/home/roberto-padilla/projects/ex47
├── bin
├── docs
├── ex47
│   ├── __init__.py
│   └── __pycache__
│       └── __init__.cpython-312.pyc
├── game.py
├── __pycache__
│   └── game.cpython-312.pyc
├── setup.py
└── tests
    ├── ex47_test.py
    ├── __init__.py
    └── __pycache__
        ├── ex47_test.cpython-312-pytest-8.3.5.pyc
        └── __init__.cpython-312.pyc

Hello,

can you try replacing this:

with this:

import os, sys

current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
sys.path.append(parent_dir)

import game

I verified its validity by testing it whereby I recreated a partial directory / module hierarchy as per your tree:

/home/roberto-padilla/projects/ex47

├── game.py
└── tests
    ├── ex47_test.py
    

In a nutshell, what you’re doing is adding the parent directory to the look-up path for this module.

I tried what you said and pytest gave me three errors. I also tried this according to Medium and the line works but pytest still gives me one error (an import error):

from ..game import Room

What you just typed is when you’re implementing relative imports. Note when you’re using relative imports, Python does not allow you to open a lower level module and run it as a standalone module. In your case, you wouldn’t be able to open the module ex47_test.py and hit run. It would give you an error. If you’re implementing relative imports, you have to implicitly run the lower level module from the main package module (script). Please take that into consideration when you’re implementing your packages.

In any case, can you try:

from pytest import *

import os, sys

current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
sys.path.append(parent_dir)

from game import Room  # Since I am assuming that you did not modify your original script

Can you try this?

If you still get errors, copy and paste it here so that we can analyze it.

I tried what you said with the exception of the line:

from game import Room

and pytest gave me three errors. This time it’s better than before! I only had one error.

=================================== FAILURES ===================================
__________________________________ test_room ___________________________________

    def test_room():
       gold = Room("GoldRoom",
                   """This room has gold in it you can grab.  There's a
                   door to the north.""")
       assert gold.name, "GoldRoom"
>      assert gold.paths, {}
E      AssertionError: {}
E      assert {}
E       +  where {} = <game.Room object at 0x7cebc39500b0>.paths

tests/ex47_test.py:15: AssertionError
=============================== warnings summary ===============================
../../.venvs/lpthw/lib/python3.12/site-packages/_pytest/terminal.py:113
  /home/roberto-padilla/.venvs/lpthw/lib/python3.12/site-packages/_pytest/terminal.py:113: PytestCollectionWarning: cannot collect test class 'TestShortLogReport' because it has a __new__ constructor (from: tests/ex47_test.py)
    class TestShortLogReport(NamedTuple):

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/ex47_test.py::test_room - AssertionError: {}
==================== 1 failed, 2 passed, 1 warning in 0.17s ====================

from pytest import *
import os, sys

current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
sys.path.append(parent_dir)

from game import Room

def test_room():
   gold = Room("GoldRoom",
               """This room has gold in it you can grab.  There's a
               door to the north.""")
   assert gold.name, "GoldRoom"
   assert gold.paths, {}
   
def test_room_paths():
    center = Room("Center", "Test room in the center.")
    north = Room("North", "Test room in the north.")
    south = Room("South", "Test room in the south.")

    center.add_paths({'north': north, 'south': south})
    assert center.go('north'), north
    assert center.go('south'), south

def test_map():
    start = Room("Start", "You can go west and down a hole.")
    west = Room("Trees", "There are trees here, you can go east.")
    down = Room("Dungeon", "It's dark down here, you can go up.")

    start.add_paths({'west': west, 'down': down})
    west.add_paths({'east': start})
    down.add_paths({'up': start})

    assert start.go('west'), west
    assert start.go('west').go('east'), start
    assert start.go('down').go('up'), start 

Ok, it is not giving you an import error. It is giving you an assertion error:

That is not three errors. That is one error and one warning:

It appears to be pointing to the following line. Can you comment it out and retry:

 assert gold.paths, {}

When I typed it in before, I left out the line:

from game import Room

That is when it gave me three errors. Not this time!

It worked, commenting out that line. But the script needed that line. I think it has something to do with the curly brackets.

Can you provide the contents of Room?

I’m not sure if this is what you want.

def test_room():
   gold = Room("GoldRoom",
               """This room has gold in it you can grab.  There's a
               door to the north.""")
   assert gold.name, "GoldRoom"
  # assert gold.paths, {}

The object Room from the module game is what I am referring to. Apparently, this:

gold.paths is False

Note that the assert keyword will only raise an exception if the conditional statement is False. If it is True, it skips the assert keyword.

I don’t know the purpose of the { } curly braces to be honest as an assignment does not take place for either a True or a False condition. Check your reference for creating this script and what did the authors have in mind for them.

https://www.datacamp.com/tutorial/understanding-the-python-assert-statement

You can try running the examples in the tutorial link that I provided as well as this example:

assert 3 < 4 , {} # "Three is less than four."

assert 3 > 4 , {} # "3 is not greater than four."

After running it, note the message that is raised. It only raises an assertion when the conditional is False.

So that it does not crash your program, you can always "catch" the assertion with a try / except combination. Play around with it to understand its behavior.

As a quick test, first run this:

assert 3 > 4 , {} # "3 is not greater than four."

then run this to compare:

try:
    assert 3 > 4 , {} # "3 is not greater than four."
    
except AssertionError:
    print('Less than error.')

Note that when you wrap it with the try / except combination, it keeps your program from crashing by catching the exception. Nice trick to consider.

Here’s the game module script:

class Room(object):

    def __init__(self, name, description):
        self.name = name
        self.description = description
        self.paths = {}

    def go(self, direction):
        return self.paths.get(direction, None)
    
    def add_paths(self, paths):
        self.paths.update(paths)    

Note that this:

self.paths = {}

is an empty dictionary. So, the assertion test just checks if it has contents or not. Here is a simple test to verify if it has contents or not.

a_dictionary = {}

if a_dictionary:
    print('Empty: False')
else:
    print('Empty: True')

a_dictionary = {1: 'some value'}  # Now has contents

if a_dictionary:
    print('Empty: False')
else:
    print('Empty: True')

In your script, you can change this:

assert gold.paths, {}

to this:

try:
    assert gold.paths
except AssertionError:
    print('self.paths is empty')  # or whatever message that suits the application
    

This way, you catch the assert conditional statement but without crashing your script.

I think you want to write:

   assert gold.name == "GoldRoom"
   assert gold.paths == {}

And so on, with ==, instead of commas, for all of your assertions.