用 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 兼容的写法。

参考

注解

这篇是个人总结的《软件构建实践》系列的一篇文章,更多更新内容,可以直接在线查看:http://pm.readthedocs.io。并且部分内容已经公布在 GitHub 上:https://github.com/akun/pm