Python测试应用与工具

时间:2022-05-03
本文章向大家介绍Python测试应用与工具,主要内容包括环境准备、unittest、pytest、mock、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

Python测试应用与公具 今天跟大家分享一个Python与测试相关的话题,主要介绍Python中的标准库 unittest及第三方测试工具pytestmock。介绍了它们的基本使用。

环境准备

  • 一台Windows机器,或一台Linux机器,亦或一台Mac
  • 安装Python,版本为2.7.x或3.5.x
  • 要有pip工具(Python2),Python3不做要求

unittest

unittest是Python标准库中用于单元测试的模块。单元测试用来对最小可测试单元进行正确性检验,帮助我们在上线之前发现问题。

接下来我们通过测试collections模块中的Counter类,先来了解unittest的用法。大家或许对collections库中Counter类不太熟悉,为了让大家更好地理解这个例子,这里简单介绍一下Counter的使用。

>>> from collections import Counter
>>> c = Counter('abcdaba') # 用来计算字符串abcdaba中各个字符出现的次数
>>> c.keys()
dict_keys(['a', 'b', 'c', 'd'])
>>> c.values()
dict_values([3, 2, 1, 1])
>>> c.elements()
<bound method Counter.elements of Counter({'a': 3, 'b': 2, 'c': 1, 'd': 1})>
>>> c['a']
3
>>> c['b']
2
>>> c['c']
1
>>> c['d']
1

一个测试脚本:

# filename: ut_case.py
import unittest
from collections import Counter

class TestCounter(unittest.TestCase):
    def setUp(self):
        self.c = Counter('abcdaba')
        print('setUp starting...')
        
    def test_basics(self):
        c = self.c
        self.assertEqual(c, Counter(a=3, b=2, c=1, d=1))
        self.assertIsInstance(c, dict)
        self.assertEqual(len(c), 4)
        self.assertIn('a', c)
        self.assertNotIn('f', c)
        self.assertRaises(TypeError, hash, c)
        
    def test_update(self):
        c = self.c
        c.update(f=1)
        self.assertEqual(c, Counter(a=3, b=2, c=1, d=1, f=1))
        c.update(a=10)
        self.assertEqual(c, Counter(a=13, b=2, c=1, d=1, f=1))
        
    def tearDown(self):
        print('tearDown starting...')


if __name__ == '__main__':
    unittest.main()

setUp方法列出了测试前的准备工作,常用来做一些初始化的工作,非必需方法。tearDown方法列出了测试完成后的收尾工作,用来销毁测试过程中产生的影响,也是非必需方法。TestCase,顾名思义表示测试用例,一个测试用例可以包含多个测试方法,每个测试方法都要以test_开头。测试方法中用到的self.assertXXX方法是断言语句,单元测试都是使用这样的断言语句判断测试是否通过的:如果断言为False,会抛出AssertionError异常,测试框架就会认为此测试用例测试失败。

运行一下上面的脚本:

(venv) C:UsersLavenLiuIdeaProjectsTestOps>python ut_case.py
setUp starting...
tearDown starting...
.setUp starting...
tearDown starting...
.
----------------------------------------------------------------------
Ran 2 tests in 0.003s

OK

可以看到每次执行test_开头的方法时,都会执行setUptearDown

pytest

Python标准库提供的测试模块功能相对单一,所以在项目中通常会额外使用第三方的测试工具。这里我们介绍pytest,pytest除了比Python标准的单元测试模块unittest更简洁和高效外,还有如下特点:

  • 容易上手,入门简单,官方文档有很多实例可供参考。
  • 可以自动发现需要测试的模块和函数。
  • 支持运行由nose、unittest等模块编写的测试用例。
  • 有很多第三方插件,并且可以方便地自定义插件。
  • 很容易与持续集成工具结合。
  • 可以细粒度地控制要测试的测试用例。

我们要先安装pytest库:

pip install pytest

接下来演示pytest常用的测试方法。一个测试用例:

# filename: test_pytest.py
import pytest


@pytest.fixture        # 创建测试环境,可以用来做setUp和tearDown的工作
def setup_math():
    import math
    return math

@pytest.fixture(scope='function')
def setup_function(request):
    def teardown_function():
        print('teardown_function called.')
    request.addfinalizer(teardown_function)  # 这个内嵌函数做tearDown工作
    print('setup_function called.')


def test_func(setup_function):
    print('Test_Func called.')


def test_setup_math(setup_math):
    # pytest不需要使用self.assertXXX这样的方法,直接使用Python内置的assert断言语句即可
    assert setup_math.pow(2, 3) == 8.0


class TestClass(object):
    def test_in(self):
        assert 'h' in 'hello'

    def test_two(self, setup_math):
        assert setup_math.ceil(10) == 10.0


def raise_exit():
    raise SystemExit(1)


def test_mytest():
    with pytest.raises(SystemExit):
        raise_exit()


@pytest.mark.parametrize('test_input, expected', [
    ('1+3', 4),
    ('2*4', 8),
    ('1==2', False),
])  # parametrize可以用装饰器的方式集成多组测试用例
def test_eval(test_input, expected):
    assert eval(test_input) == expected

unittest必须把测试放在TestCase类中,pytest只要求测试函数或者类以test开头即可。运行一下上面的脚本:

(venv) C:UsersLavenLiuIdeaProjectsTestOps>py.test test_pytest.py
============================= test session starts =============================
platform win32 -- Python 3.6.3, pytest-3.2.3, py-1.4.34, pluggy-0.4.0
rootdir: C:UsersLavenLiuIdeaProjectsTestOps, inifile:
plugins: xdist-1.20.1, random-0.2, metadata-1.5.0, instafail-0.3.0, html-1.16.0, forked-0.2
collected 12 items

test_pytest.py ............
========================== 12 passed in 0.05 seconds ==========================

测试通过,我们让其中一个测试用例测试失败:

(venv) C:UsersLavenLiuIdeaProjectsTestOps>py.test test_pytest.py
============================= test session starts =============================

platform win32 -- Python 3.6.3, pytest-3.2.3, py-1.4.34, pluggy-0.4.0
rootdir: C:UsersLavenLiuIdeaProjectsTestOps, inifile:
plugins: xdist-1.20.1, random-0.2, metadata-1.5.0, instafail-0.3.0, html-1.16.0, forked-0.2
collected 8 items

test_pytest.py ...F....
================================== FAILURES ===================================
_____________________________ TestClass.test_two ______________________________
self = <test_pytest.TestClass object at 0x0000000003837588>
setup_math = <module 'math' (built-in)>

    def test_two(self, setup_math):
>      assert setup_math.ceil(10) == 11.0
E      AssertionError: assert 10 == 11.0
E        +  where 10 = <built-in function ceil>(10)E        
+    where <built-in function ceil> = <module 'math' (built-in)>.ceil

test_pytest.py:31: AssertionError
===================== 1 failed, 7 passed in 0.08 seconds ======================

pytest帮助我们定位到测试失败的位置,并告诉我们预期值和实际值。pytest的命令行功能非常丰富:

# 与使用pytest的作用一样
python -m pytest test_pytest.py
# 验证整个目录
pytest /path/to/test/dir
# 只验证文件中的单个测试用例,这在实际工作中非常方便,
# 否则可能需要运行一段时间才能轮到有问题的测试用例,极为浪费时间。
# 使用这样的方式就可以有针对性地验证有问题的测试用例
pytest test_pytest.py::test_mytest
# 只验证测试类中的单个方法
pytest test_pytest.py::TestClass::test_in

pytest插件

pytest有丰富的插件,这里列出几个常用的pytest插件,pytest插件都是以pytest-开头。

  • pytest-random:可以让测试变得随机。当有很多测试用例时,这个插件不会让测试只卡在一个异常上,有助于发现其他异常。
  • pytest-xdist:让pytest支持分布式测试
  • pytest-instafail:一旦出现错误信息就立即返回,不需要等到全部测试结束后才显示。
  • pytest-html:可以生存测试报告文件。

mock

Mock测试是在测试过程中对可能不稳定、有副作用、不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便完成测试的方法。在Python中,这种测试是通过第三方的mock库完成的,mock在Python3.3的时候被引入到了Python标准库中,改名为unittest.mock。之前的Python版本都需要安装它:

pip install mock

假设现在一个单元测试依赖外部的API返回值。举个例子(client.py):

# filename: client.py
import requests


def api_request(url):
    r = requests.get(url)
    return r.json()


def get_review_author(url):
    res = api_request(url)
    return res['review']['author']

如果在测试时,每次都真正请求这个接口,就会有两个问题:

  • 测试环境可能和线上环境不同,需要搭建本地的API服务,尤其是需要本地环境能返回线上环境实际的全部结果,增加复杂度且效率低下。
  • 测试结果严重依赖外部API服务的稳定性。

使用mock的解决方案如下(test_mock.py):

# filename: test_mock.py
import unittest
import mock
import client


class TestClient(unittest.TestCase):
    def setUp(self):
        self.result = {'review': {'author': 'testops'}}

    def test_request(self):
        api_result = mock.Mock(return_value=self.result)
        client.api_request = api_result
        self.assertEqual(client.get_review_author(
            'http://api.testops.cn/review/123'), 'testops')

运行一下看看效果:

(venv) C:UsersLavenLiuIdeaProjectsTestOps>pytest test_mock_py3.py
============================= test session starts =============================

rootdir: C:UsersLavenLiuIdeaProjectsTestOps, inifile:
plugins: xdist-1.20.1, random-0.2, metadata-1.5.0, instafail-0.3.0, html-1.16.0, forked-0.2
collected 1 item

test_mock_py3.py .
========================== 1 passed in 0.36 seconds ===========================

可以看到,这个测试并没有实际地请求API就达到了测试的目的。

好的,今天就介绍这么多,后续会继续更新。请大家多多关注。