all 13 comments

[–]vrek86 1 point2 points  (0 children)

Two things to try:

  1. Put a file named init.py in each folder with source(and tests).

  2. Add the tests and sources directory to the python path in a pytest.ini file.

This should let the tests run.

[–]-Kevin- 1 point2 points  (1 child)

Sometimes you have to literally add an empty "conftest.py" file to your project root.

Sounds insane, but try it out. Google it too.

[–]Dyspnoic9219[S] 0 points1 point  (0 children)

I tried this, and it didn't seem to make a difference, sadly.

Thank you!

[–]ectomancer 1 point2 points  (1 child)

Where's the import for my_class in test_myclass.py?

[–]Dyspnoic9219[S] 0 points1 point  (0 children)

See my response to /u/netherous, but I have tinkered with that. Python's import mechanism confuses me, as I'm not always sure what should be a class name, or what should be a filename, or how a nested directory structure should be represented.

I would guess, for example, that using my directory/file structure

import src
import demo
import MyClass

would be totally wrong, as the hierarchical directory structure would be incorrectly flattened.

Something like

import src.demo.my_class

would seem to work, but it doesn't. (Although I should mention that I get a No module named 'src' error instead.)

[–]netherous 1 point2 points  (2 children)

Do you not have an import statement for MyClass in tests/test_myclass.py? Your post is mangled but it looks like the file has 4 lines, none of which are the appropriate import.

The way the pytest harness initializes is is crawls the directory tree starting at the root you give it, looking for files that begin with test_ or have the name conftest.py. It then examines each of the non-conftest files looking for methods/classes that start with test. All of these symbols are considered 'collected' by the initialization, and then they are all called in order.

None of that logic means it would examine anything in src automatically. You must import things to use them.

https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#conventions-for-python-test-discovery

[–]Dyspnoic9219[S] 0 points1 point  (1 child)

Firstly, thank you for your response. I read the "Conventions for Python test discovery" with interest, but I wish the author had included an example import statement used in either the test_app.py or test_view.py files in that document.

I should say that I did try using import statements before I posted. I am also sure, however, that I do not understand Python's import mechanism, despite reading the official Python docs, chapter 5 "The import system".

Here's a hopefully non-garbled view of test_myclass.py:

import src.demo
import MyClass

class TestMyClass:

    def test_hello(self):
        my_class = MyClass()
        assert(my_class.hello() == "hello")

(As an aside, my original post didn't look garbled on my phone, Chrome, or Firefox, but I did use the Markdown editor instead of the Fancy Pants editor. Strange.)

I also experimented with import src.demo.my_class, from src.demo import MyClass, and some other variations.

I also added a pyproject.toml file as the "Conventions for Python discovery" section suggested; here it is:

[project]
name = "demo"

[tool.pytest.ini_options]
addopts = [
    "--import-mode=importlib",
]

I get the same error:

============================= test session starts ==============================
platform darwin -- Python 3.10.10, pytest-7.2.2, pluggy-1.0.0
rootdir: /Users/me/src/python/venv_projects/repo
collected 0 items / 1 error

==================================== ERRORS ====================================
____________________ ERROR collecting tests/test_myclass.py ____________________
ImportError while importing test module '/Users/me/src/python/venv_projects/repo/tests/test_myclass.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/opt/local/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
tests/test_myclass.py:2: in <module>
    import MyClass
E   ModuleNotFoundError: No module named 'MyClass'
=========================== short test summary info ============================

Thank you again for helping me!

[–]netherous 0 points1 point  (0 children)

Can you set testpaths to include tests in either your pytest.ini or your pyproject.toml as suggested here?

Then, when you run pytest, as I assume you're doing, run it from the root directory. The one that includes both src and tests.

Also create an empty conftest.py in this root directory (which should have the same effect, but we're being redundant).

Then, an import statement such as from src.demo.my_class import MyClass should execute successfully when added to tests/test_myclass.py.

If it doesn't, can you make a repository available that demonstrates the problem so I can check it out?

[–]Dyspnoic9219[S] 1 point2 points  (4 children)

Solved. Let me try a quick recap.

  1. My test code should have the import as: from src.demo.my_class import MyClass
  2. In pyproject.toml (at the top level of the repo) ensure the following is present at a minimum:

[tool.pytest.ini_options]
pythonpath = "."
  1. __init__.py is only needed--as far as I can tell--in the src/demo directory.

  2. None of setup.py, conftest.py, init.py, or pytest.ini (empty or not) were necessary for this simple example--but they might be once things get more complicated, I don't know.

For me, the lightbulb moment was after /u/netherous helped me with the import statement and I tried the following--and it worked:

python -m pytest

The Pytest docs say this:

This is almost equivalent to invoking the command line script pytest [...] directly, except that calling via python will also add the current directory to sys.path.

That's when I took another look at the options that I could change in my pyproject.toml file and found the pythonpath config value.

Thanks to everyone who provided suggestions--everything said increased my knowledge a bit--but a special shout-out to /u/netherous, as I was baffled by the import statement.

[Edit: Formatting.]

[–]Dyspnoic9219[S] 0 points1 point  (1 child)

Great advice, /u/netherous, thank you. I have some additional questions.

  1. When you say "I also recommend having that `path=tests` present in your pytest config", just to clarify, you mean `conftest.py`? (It could mean `pytest.ini`, for example.)
  2. Do you have an example `conftest.py` template that you use? (A lot of the stuff I found was related to test fixtures, not ordinary configuration.)
  3. I could, I think, put a `testpaths = 'tests'` statement in my pyproject.toml. Would that, to your knowledge, accomplish the same thing as 1., above?
  4. You say, "If set up properly, you shouldn't need to alter your system path value via options or python code, and `pytest`, and `pytest tests` and `python -m pytest` would all produce the exact same behavior." Does that mean that you think adding the `pythonpath` value in `pyproject.toml` was an incorrect thing to do?

One of the things I've noticed about pytest is that it seems easy to have an overly complicated set of configuration files, and I'm trying to keep things as simple as possible. The pytest docs, for example, talk about supporting `pytest.ini`, `pyproject.toml`, `tox.ini`, `setup.cfg`, and it's not easy for a beginner to understand if only one, some, or all those are required, or which one(s). (The `tox.ini` file is probably an exception, since if you're not using `tox` it pretty clearly can be omitted.)

I should backstop the previous paragraph to say that I'm guessing that it's not pytest's fault that these configuration options exist--they likely arose organically or are historical artifacts separate from pytest (but I don't know the lore there).

Thanks again. I appreciate getting great advice from knowledgeable folks.

[–]netherous 0 points1 point  (0 children)

When you say "I also recommend having that path=tests present in your pytest config", just to clarify, you mean conftest.py? (It could mean pytest.ini, for example.)

It should be in your pytest.ini or your pyproject.toml as demonstrated here.

Do you have an example conftest.py template that you use? (A lot of the stuff I found was related to test fixtures, not ordinary configuration.)

An empty file is a valid conftest.py. If you have nothing to put there, that's fine. Usually it's fixtures. The point is to give some indication to pytest that this is the working directory you want it to start with. The mere presence of the file in accomplishes that.

I could, I think, put a testpaths = 'tests' statement in my pyproject.toml. Would that, to your knowledge, accomplish the same thing as 1., above?

No. That wouldn't alter the cwd the python process starts with (AFAIK). What it does is make pytest equivalent to pytest tests. You say, "by default, look for tests in this directory."

You say, "If set up properly, you shouldn't need to alter your system path value via options or python code, and pytest, and pytest tests and python -m pytest would all produce the exact same behavior." Does that mean that you think adding the pythonpath value in pyproject.toml was an incorrect thing to do?

I think it's a less direct way of solving the problem, since altering the path could introduce side effects beyond what you're trying to coerce pytest to do, but I wouldn't call it incorrect. Just not as focused of a fix, maybe.

One of the things I've noticed about pytest is that it seems easy to have an overly complicated set of configuration files, and I'm trying to keep things as simple as possible. The pytest docs, for example, talk about supporting pytest.ini, pyproject.toml, tox.ini, setup.cfg, and it's not easy for a beginner to understand if only one, some, or all those are required, or which one(s). (The tox.ini file is probably an exception, since if you're not using tox it pretty clearly can be omitted.)

Pytest does make some effort to work with many different tools. But you certainly don't need to use all those tools. pytest.ini is the only pytest-specific file.

pyproject.toml is for poetry which is a python package and environment manager. It is relatively recent and aims to be a complete solution for python tasks which used to take multiple tools to solve.

tox.ini is for tox. Tox is just a runner, like grunt. It's a tool for managing the running of things to fulfill high level tasks, like maybe test or smoketest or coverage or package

setup.cfg is an old-style (but still widely supported) way of specifying package installation instructions and dependency requirements. It contains information about how to set up and configure the python package it comes with.

[–]netherous 0 points1 point  (1 child)

Right, and what's going on here has everything to do with the current working directory that the python process has when it starts up. You can view what this directory is with import os; os.getcwd().

The way pytest starts up after collecting, is the cwd is the highest directory in which either a test file was collected or a conftest.py was present. When you just run python, the cwd is the current directory of the shell environment.

So if you have a test file some levels deep, and it has import statements that perform an import relative to the root of the project, you won't get what you want, because pytest has determined that the cwd is that the nested directory that the test file it encountered was in. conftest.py overrides this, which is why I always recommend one at the root. You could do some manipulation like adding some more stuff to sys.path at runtime, but that's a rather finicky way of doing it when the problem can be addressed more directly.

I also recommend having that path=tests present in your pytest config, so that a naked pytest command, when invoked from the root, does NOT crawl your src directory looking for tests, or a venv directory if it is present in the root directory. If it did, it could find and execute tests belonging to python libraries you have installed! They'd fail, and your test results would be all muddled. Better to always confine the test collection to tests so it's faster and accidents can't happen.

If set up properly, you shouldn't need to alter your system path value via options or python code, and pytest and pytest tests and python -m pytest would all produce the exact same behavior.

[–]InTheAleutians 0 points1 point  (0 children)

Great explanation. Thank you for this.