用 tox 进行 Python 2 和 Python 3 的兼容性测试
背景
不少 Python 项目会遇到从 Python 2 到 Python 3 过渡的问题,由于过渡需要持续一段时间,所以代码得保证在 Python 2 和 Python 3 中都能运行正常。为了解决这个问题,所以需要引入 tox 这个工具。
tox 简介
tox 项目代码地址:https://github.com/tox-dev/tox
按官网描述,该项目目前是为了自动化和标准化 Python 的测试工作。主要整合了 2 块内容:分别是,virtualenv 管理不同版本 Python,以及结合不同测试工具命令行调用。
tox 有以下用处:
- 用不同 Python 版本的解释器,来检查 Python 包是否能正确安装;
- 调用你选择的测试工具,在不同 Python 版本的环境下运行测试;
- 更方便集成到 CI(持续集成)中,减少或合并一些可能会产生的重复内容。
使用说明
安装
pip install tox
可以把 tox 纳入你的工程项目的研发环境常用依赖库。
配置
用向导工具生成 tox 配置
tox-quickstart
或者直接编辑新建 tox.ini 文件,常见配置如下:
[tox]
envlist = py27, py34, py35, py36
[testenv]
deps =
coverage
nose
commands = nosetests -c nose.cfg
相当于声明了会在 Python 2.7、Python 3.4、Python 3.5、Python 3.6 这 4 个不同环境下进行:发布、安装、测试相关的工作。
这里因为包测试需要依赖:coverage 和 nose,分别进行测试覆盖率统计和方便测试的调用。
这里借用了 nose 进行测试,所以执行的对应测试命令就是 nosetests,具体测试配置写在 nose.cfg 里,后续单独开个主题讲下 nose 的使用。
运行
tox
没错,就这么简单,如果不想细究更多参数的话。如果运行没问题就会输出类似信息:
...
py27: commands succeeded
py34: commands succeeded
py35: commands succeeded
py35: commands succeeded
congratulations :)
表示在这些 Python 解释器环境下能执行通过,说明可以兼容这些版本 Python 解释器。
例子
用一个小的 Python 工程项目举例说明并感受下 tox 的使用,具体如下:
main.py
#!/usr/bin/env python
# coding=utf-8
from __future__ import print_function, unicode_literals
from future import standard_library
standard_library.install_aliases()
import urllib.request
def do_print_example():
text = '海贼王(One Piece)'
print(text)
return text
def do_numliterals_example():
number = 0o755
return number
def do_except_example():
try:
number = 1 / 0
except ZeroDivisionError as ex:
number = None
return number
def do_raise_example():
raise Exception('I am Exception')
def do_urllib_example():
response = urllib.request.urlopen('http://www.google.com')
return response
test_main.py
#!/usr/bin/env python
# coding=utf-8
from __future__ import unicode_literals
import unittest
import httpretty
from onepiece import main
class MainTestCase(unittest.TestCase):
def test_do_print_example(self):
text = main.do_print_example()
self.assertEqual(text, '海贼王(One Piece)')
def test_do_numliterals_example(self):
number = main.do_numliterals_example()
self.assertEqual(number, 0o755)
def test_do_except_example(self):
number = main.do_except_example()
self.assertIsNone(number)
def test_do_raise_example(self):
with self.assertRaises(Exception) as cm:
main.do_raise_example()
ex = cm.exception
self.assertEqual(ex.args[0], 'I am Exception')
@httpretty.activate
def test_do_urllib_example(self):
httpretty.register_uri(
httpretty.GET, 'http://www.google.com', body='Fake Google',
status=200)
response = main.do_urllib_example()
self.assertEqual(response.code, 200)
setup.py
#!/usr/bin/env python
# coding=utf-8
from setuptools import find_packages, setup
from onepiece import __version__
setup(
name='onepiece',
version=__version__,
description='One Piece',
author='akun',
author_email='6awkun@gmail.com',
license='MIT License',
url='https://github.com/akun/pm/tree/master/pm/onepiece',
packages=find_packages(),
include_package_data=True,
install_requires=[
'future>=0.16.0',
],
extras_require={
'test': [
'coverage>=4.5',
'httpretty>=0.8.14',
'nose>=1.3.7',
],
},
test_suite='nose.collector',
classifiers=[
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
]
)
tox.ini
[tox]
envlist = py27, py3
[testenv]
deps =
coverage
httpretty
nose
commands = nosetests -c nose.cfg
运行结果
命令行下执行 tox,输出示例如下:
GLOB sdist-make: /home/yourname/projects/pm/pm/onepiece/setup.py [0/4158]
py27 create: /home/yourname/projects/pm/pm/onepiece/.tox/py27
py27 installdeps: coverage, httpretty, nose
py27 inst: /home/yourname/projects/pm/pm/onepiece/.tox/dist/onepiece-0.1.0.zip
py27 installed: coverage==4.5,future==0.16.0,httpretty==0.8.14,nose==1.3.7,onepiece==0.1.0,pkg-resources==0.0.0
py27 runtests: PYTHONHASHSEED='3715476856'
py27 runtests: commands[0] | nosetests -c nose.cfg
..海贼王(One Piece)
...
Name Stmts Miss Cover
------------------------------------------
onepiece/__init__.py 1 0 100%
onepiece/main.py 22 0 100%
------------------------------------------
TOTAL 23 0 100%
----------------------------------------------------------------------
Ran 5 tests in 0.061s
OK
py3 create: /home/yourname/projects/pm/pm/onepiece/.tox/py3
py3 installdeps: coverage, httpretty, nose
py3 inst: /home/yourname/projects/pm/pm/onepiece/.tox/dist/onepiece-0.1.0.zip
py3 installed: coverage==4.5,future==0.16.0,httpretty==0.8.14,nose==1.3.7,onepiece==0.1.0,pkg-resources==0.0.0
py3 runtests: PYTHONHASHSEED='3715476856'
py3 runtests: commands[0] | nosetests -c nose.cfg
..海贼王(One Piece)
...
Name Stmts Miss Cover
------------------------------------------
onepiece/__init__.py 1 0 100%
onepiece/main.py 22 0 100%
------------------------------------------
TOTAL 23 0 100%
----------------------------------------------------------------------
Ran 5 tests in 0.034s
OK
_______________________________________________________________________________ summary ________________________________________________________________________________
py27: commands succeeded
py3: commands succeeded
congratulations :)
完整示例
可以实际演练下,更方便理解,可以在这里查看更完整示例:https://github.com/akun/pm/tree/master/pm/onepiece
总结
简单总结下:
- tox 可以让你更轻松地在不同 Python 版本的解释器上进行测试;
- tox 只是个辅助工具,关键还是得有足够单元测试代码覆盖来检验 Python 2 和 Python 3 兼容;
- tox 使用起来,可以把更多重点放到 Python 2 和 Python 3 兼容的写法上。后续单独开个主题讲下 Python 2 和 Python 3 兼容的写法。
参考
- https://github.com/tox-dev/tox
- https://tox.readthedocs.io/
- https://docs.python.org/3/library/unittest.html
- http://python-future.org/quickstart.html
注解
这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.io。并且部分内容已经公布在 GitHub 上:https://github.com/akun/pm