Python自定义包下不同目录单元测试的导入错误
需求驱动学习。
前言
嗯,很绕口的标题
最近的项目需要把编写的工具放到tools目录,把单元测试放到test目录,造成了不同目录下导入(import)的错误。基础原因很简单,Python无法找到要导入的文件,而解决这个问题的根本方法,是向sys.path
中添加搜索路径,如果手动添加,太俗了不是么。
所以,本文内容为包(Package)和单元测试的结合笔记。从基本的单元测试,到多目录级的单元测试。单元测试使用PyUnit和nose。
所以,假设你已经有编写过单元测试的基础经历,但nose可以不了解,只需要知道如何安装和运行即可,若不了解,可参考本页面。
实验
为了方便些笔记,该实验都是在Window上进行的,实验4为了使用nose,nose部分在Ubuntu上进行。
1. 基础导入测试
当前文件结构如下:
1 | foo |
bar.py
内容如下:
1 | def dumb_true(): |
test_bar.py
内容如下:
1 | import unittest |
运行命令:
1 | cd foo |
python
命令会将运行文件所在目录加到sys.path
中,因此python可以搜索到模块bar.py
,所以导入成功。
运行结果:无错误提示, 能导入模块bar。
2. 基础单元测试
修改test_bar.py
内容为:
1 | import unittest |
或者
1 | import unittest |
运行命令:
1 | cd foo |
运行结果:单元测试成功,说明可以从test_bar.py
内调用bar.dumb_true
函数。
3. 不同目录下的单元测试
这起源于构建package后,通常把单元测试放到单独的tests
目录。tests在包的外面,和包在相同的目录。
目录结构如下:
1 | my_project |
其中,my_project
是你的项目目录,foo
是包目录。tests
目录存放了对工程的单元测试。foo
目录下的__init__.py
使得,foo是一个包,也就说目录下有__init__.py
的目录都是包。
在本实验中单元测试的对象是包foo
下的bar.py
文件。
bar.py
文件内容如实验1与实验2,在本系列实验中,bar.py
的内容始终保持不变。
__init__.py
和test_foo.py
内容全部为空。
修改test_bar.py
内容为:
1 | import unittest |
运行命令:
1 | cd my_project |
unittest的命令行接口,会把当前的路径(运行python -m unittest
命令的路径,关于该验证,可以看本文末尾)加入到sys.path
中,因此Python可以搜索到包foo
,所以在test_bar.py
中,可以直接使用
1 | from foo import bar |
运行结果:单元测试成功,说明可以从test_bar.py
内调用foo.bar.dumb_true
函数。
但是当我把test_bar.py
修改为如下时:直接导入dumb_true函数,出现了一个错误。
1 | import unittest |
错误是:
1 | E |
从ImportError: cannot import name dumb_tree
可以看出,是我的拼写错误,但是我找了几次才发现这个错误的,错误总是发生在意想不到的地方。
也许你认为这个错误很Silly,但我认为,拼写错误是常见的,但看了几次才发现错误,确实很Silly。
将dumb_tree
改为dumb_true
后,单元测试成功。
4. 如果my_project是一个包呢
如果我将my_project改为一个包,tests是包的一部分。为了理解,我将my_project改为my_package。
目录结构如下:
1 | my_package |
这些my_package下多了一个./__init__.py
,它变成包了。
现在不修改任何内容,我们重新运行单元测试命令,看会得到什么结果。
单元测试成功,因为依然遵循了unittest的命令行接口,会把当前的路径加入到sys.path
中的原则。
假若我将test_bar.py
修改为:
1 | import unittest |
我们重新运行单元测试命令,得到了错误:
1 | E |
错误显示,没有被命名为my_package.foo.bar
的模块,说白了,是他没找到这个模块。那么怎么才能成功运行单元测试呢?既然本质是没有在搜索路径中,那么只要能让my_package在搜索路径中即可。
猜测解决方案1:在my_package的父目录运行单元测试命令。经测试成功。
猜测解决方案2:修改文件,手动将my_package
的父目录添加到sys.path
中。
在test_bar.py
行首添加:
1 | import sys |
然后
1 | cd tests |
单元测试成功。
为什么我不是这样做呢?
1 | cd my_package |
这样本错误依然存在。在实验1中提到,python会搜索文件所在的路径,而不是添加到sys.path
中,如今,我使用的是相对于test_bar.py
的路径,必须在tests目录运行,python才会搜索test_bar.py
的祖父目录,这样才能找到my_package.foo.bar
。在my_packge目录中运行,my_package的祖父目录找不到my_package.foo.bar
。
猜测解决方案3:使用nose,nose很人性化,它会将运行nosetests的目录,及其子目录下所有测试文件,所引用到的模块,自动加入到sys.path
中,使用nose通常很少遇到导入问题。*该方案是在Ubuntu下实验的**。
附件测试:unittest命令行接口会改变sys.path
目录结构如下:
1 | outter_dir |
test.py
内容如下:
1 | from pprint import pprint |
1. python命令:
1 | cd outter_dir |
结果中包含的是
1 | C:\\Users\\Brave\\PycharmProjects\\learn-python\\py2\\test\\outter_dir\\inner_dir |
1. python -m unittest命令:
1 | cd outter_dir |
结果中包含的是
1 | C:\\Users\\Brave\\PycharmProjects\\learn-python\\py2\\test\\outter_dir |
本实验证明了,python
命令会将运行文件所在目录加到sys.path
中,而python -m unittest
命令,将运行命令所在目录加入到sys.path
中。
包与单元测试的实验终结
到目前的实验为止,已经知道相同目录下的单元测试,单元测试在包外,单元测试在包内的三种情况,及相应的单元测试方法。
其实,本质上讲,还是要让导入的包处于搜索路径内,所以,无论是如何放置单元测试,一定要让他们在搜索路径内。实验3与实验4是两种常用的目录组织方式,实验4需要稍作处理,sklearn使用的即实验4的组织方式,它的解决方案是修改了顶层__init__.py
做了处理,但我还没有搞懂,有兴趣的可访问:
https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/__init__.py 。
相关文章:对自定义包的引用