I'm running into a mysterious import error when using nosetests to run a test suite that I can't reproduce outside of the nose. Furthermore, the import error disappears when I skip a subset of the tests.
Executive Summary: I am getting an import error in Nose that a) only appears when tests bearing a certain attribute are excluded and b) cannot be reproduced in an interactive python session, even when I ensure that the sys.path is the same for both.
Details:
The package structure looks like this:
project/
module1/__init__.py
module1/foo.py
module1/test/__init__.py
module1/test/foo_test.py
module1/test/test_data/foo_test_data.txt
module2/__init__.py
module2/bar.py
module2/test/__init__.py
module2/test/bar_test.py
module2/test/test_data/bar_test_data.txt
Some of the tests in foo_test.py are slow, so I've created a @slow decorator to allow me to skip them with a nosetests option:
def slow(func):
"""Decorator sets slow attribute on a test method, so
nosetests can skip it in quick test mode."""
func.slow = True
return func
class TestFoo(unittest.TestCase):
@slow
def test_slow_test(self):
load_test_data_from("test_data/")
slow_test_operations_here
def test_fast_test(self):
load_test_data_from("test_data/")
When I want to run the fast unit tests only, I use
nosetests -vv -a'!slow'
from the root directory of the project. When I want to run them all, I remove the final argument.
Here comes the detail that I suspect is to blame for this mess. The unit tests need to load test data from files (not best practice, I know.) The files are placed in a directory called "test_data" in each test package, and the unit test code refers to them by a relative path, assuming the unit test is being run from the test/ directory, as shown in the example code above.
To get this to work with running nose from the root directory of the project, I added the following code to init.py in each test package:
import os
import sys
orig_wd = os.getcwd()
def setUp():
"""
test package setup: change working directory to the root of the test package, so that
relative path to test data will work.
"""
os.chdir(os.path.dirname(os.path.abspath(__file__)))
def tearDown():
global orig_wd
os.chdir(orig_wd)
As far as I understand, nose executes the setUp and tearDown package methods before and after running the tests in that package, which ensures that the unit test can find the appropriate test_data directory, and the working directory is reset to the original value when the tests are complete.
So much for the setup. The problem is, I get an import error only when I run the full suite of tests. The same modules import just fine when I exclude the slow tests. (To clarify, the tests throwing import errors are not slow, so they execute in either scenario.)
$ nosetests
...
ERROR: Failure: ImportError (No module named foo_test)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Library/Python/2.7/site-packages/nose/loader.py", line 413, in loadTestsFromName
addr.filename, addr.module)
File "/Library/Python/2.7/site-packages/nose/importer.py", line 47, in importFromPath
return self.importFromDir(dir_path, fqname)
File "/Library/Python/2.7/site-packages/nose/importer.py", line 80, in importFromDir
fh, filename, desc = find_module(part, path)
ImportError: No module named foo_test
If I run the test suite without the slow tests, then no error:
$ nosetests -a'!slow'
...
test_fast_test (module1.test.foo_test.TestFoo) ... ok
In a python interactive session, I can import the test module with no trouble:
$ python
Python 2.7.1 (r271:86832, Aug 5 2011, 03:30:24)
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import module1.test
>>> module1.test.__path__
['/Users/USER/project/module1/test']
>>> dir(module1.test)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'orig_wd', 'os', 'setUp', 'sys', 'tearDown']
When I set a breakpoint in nose/importer.py, things look different:
> /Library/Python/2.7/site-packages/nose/importer.py(83)importFromDir()
-> raise
(Pdb) l
78 part, part_fqname, path)
79 try:
80 fh, filename, desc = find_module(part, path)
81 except ImportError, e:
82 import pdb; pdb.set_trace()
83 -> raise
84 old = sys.modules.get(part_fqname)
85 if old is not None:
86 # test modules frequently have name overlap; make sure
87 # we get a fresh copy of anything we are trying to load
88 # from a new path
(Pdb) part
'foo_test'
(Pdb) path
['/Users/USER/project/module1/test']
(Pdb) import module1.test.foo_test
*** ImportError: No module named foo_test
#If I import module1.test, it works, but the __init__.py file is not being executed
(Pdb) import partition.test
(Pdb) del dir
(Pdb) dir(partition.test)
['__doc__', '__file__', '__name__', '__package__', '__path__'] #setUp and tearDown missing?
(Pdb) module1.test.__path__
['/Users/USER/project/module1/test'] #Module path is the same as before.
(Pdb) os.listdir(partition.test.__path__[0]) #All files are right where they should be...
['.svn', '__init__.py', '__init__.pyc', 'foo_test.py', 'foo_test.pyc','test_data']
I see the same screwy results even if I copy sys.path from my interactive session into the pdb session and repeat the above. Can anyone give me any insight about what might be going on? I realize I'm doing several non-standard things at the same time, which could lead to strange interactions. I'd be as interested in advice on how to simplify my architecture as I would be to get an explanation for this bug.
Here is how to track down the context of the error.
nosetests --debug=nose,nose.importer --debug-log=nose_debug <your usual args>
Afterwards, check the nose_debug
file. Search for your error message "No module named foo_test
". Then look at the preceding few lines to see which files/directories nose was looking at.
In my case, nose was attempting to run some code which I had imported into my codebase - a 3rd party module which contained its own tests, but which I was not intending to include in my test suite. To resolve this, I used the nose-exclude plugin to exclude this directory.
It's just nose adjusting your path by default. It will change sys.path before importing your module, possibly allowing double code execution and imports outside of package (like your case).
To avoid this, setup your PYTHONPATH
before running nose and use nose --no-path-adjustment
. See: http://nose.readthedocs.org/en/latest/usage.html#cmdoption--no-path-adjustment
If you cannot add a command line argument you can use an env var (NOSE_NOPATH=y
) or this in .noserc
:
[nosetests]
no-path-adjustment=1
I encountered this problem, and traced it to 1) forgetting to activate the virtualenv I was using, and 2) my shell, zsh, apparently having cached the path to the wrong instance of the nosetests
executable on my machine.
Once I activated my virtualenv, then gave the shell command hash -r
, this error stopped occurring. Sorry, I didn't pin down whether only one of those would have been sufficient.
I found this reply by raffienficiaud, to nose issue "nosetest does not honour virtual environments", helpful:
For the record, it is a
bash
issue that caches commands. In that case,which nosetests
points (deterministically) to the right executable, whilebash
cached the system installed one. Usinghash -r
clears the cache (see http://unix.stackexchange.com/questions/5609/how-do-i-clear-bashs-cache-of-paths-to-executables)
That Unix.SE answer is to a question, "How do I clear Bash's cache of paths to executables?", by Tobu and Zigg.
bash
does cache the full path to a command. You can verify that the command you are trying to execute is hashed with thetype
command:
$ type svnsync svnsync is hashed (/usr/local/bin/svnsync)
To clear the entire cache:
$ hash -r
Or just one entry:
$ hash -d svnsync
For additional information, consult
help hash
andman bash
.
I use zsh
not bash
, and hash -d nosetests
gave me an error message. Nevertheless, the problem was gone after I did hash -r
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With