Python测试开发1 - fixture

Posted on | 2275 words | ~5 mins
Python Test

资深开发者实际时间分配有可能是4分调研+设计,3分编码,3分测试。且越是老鸟,测试比重越高。测试下功夫了,质量就到位了,返工次数少,调试难度低,工效KPI也就高了。本文分享Python测试开发中的一些心得。

Python测试开发通常以“单元测试”的形式体现。常用的单元测试框架有unittestnosepytest。其中 pytest功能丰富、成熟度高、普及度高,且社区始终维护更新。

pytest通过pytest-mock提供了mocker fixture模拟外部依赖。pytest-mock底层用的是unittest.mock(见代码)。因此,我选用pytest+ 原生unittest.mock作为测试开发组合。此外,还会用tox作为单元测试驱动框架。

单元测试工具并不复杂,就不详细介绍了。本系列文章重点放在两大神器fixturemock上。

单元测试工具不复杂,不代表单元测试不复杂。单元测试的复杂性更多体现在:测试用例设计、测试数据设计和准备、如何减少测试代码对外部系统的依赖,以及降低待测部分持续变化对测试代码的冲击,等“设计”层面问题。

关于fixture

单元测试传递同样的输入到目标代码,预期目标代码输出同样的结果,从而判断测试通过与否。为了达到“同样的输入”这个目标,通常需要测试执行前(后),执行同样的初始化(清理)逻辑。被抽离封装后的初始化和清理逻辑,可称为fixturepytest中使用fixture十分简单,可参考帮助文档。下文只讲几个不关注就掉坑的问题。

fixture被执行几次?

在下面的代码中,foo2foo3都引用了foo1, 当pytest执行该测试模块时,到底foo1被执行几次?

 1# -*- encoding: utf-8 -*-
 2import pytest
 3
 4@pytest.fixture
 5def foo1():
 6    print("foo1")
 7
 8@pytest.fixture
 9def foo2(foo1):
10    print("foo2")
11
12@pytest.fixture
13def foo3(foo1):
14    print("foo3")
15
16def test_foo(foo2, foo3):
17    assert True
18
19def test_foo2(foo2):
20    assert True
21
22def test_foo3(foo3):
23    assert True

试一下:

 1$ pytest ./test.py  ; 假设测试模块名为test.py
 2...
 3
 4test.py::test_foo2
 5foo1
 6foo2
 7PASSED
 8
 9test.py::test_foo3
10foo1
11foo3
12PASSED
13
14test.py::test_foo
15foo1
16foo2
17foo3
18PASSED

观察输出会有如下结论:

  1. 每个test_*都会执行一次foo1。因此不用担心一个测试修改了fixture,而污染另一个测试的问题。
  2. 对于同一个test_*,即使foo1被引用多次,也只会执行一次。之后的引用,都会得到第一次执行结果的缓存 。所以务必小心:在不得不多次引用时,一旦某个引用改变了fixture, 则所有引用都会改变(因为大家用的是同一个对象)。

fixture在何时执行?

毫无疑问,fixture在每个测试前才会被执行。没有被用到的fixture不会被执行。依旧使用test.py中的fixture, 但测试方法调整为:

1def test_foo1(foo1):
2    assert True
3
4def test_foo3(foo3):
5    assert True

执行时,先打印了测试方法名称,然后才打印出fixture中的内容;同时foo2没有引用,没有打印,说明没有执行:

1test.py::test_foo3
2foo1
3foo3
4PASSED
5
6test.py::test_foo1
7foo1
8PASSED

scope和autouse

上文提到fixture每个测试前都执行,其实这不是全部真相。毕竟有些fixture是重量级的,每个方法执行一次开销太大,得不偿失。fixture有两个重要参数scopeautouse, 结合使用可以控制fixture执行的时机和次数。

autouse不言自明。设置autouseTruefixture即使没有被测试方法引用,也会执行。例如:每个测试都用到了数据库。为了避免测试相互干扰,执行每个测试前,需要清理数据库。每个方法签名上增加cleaned_dbfixture固然可以,但更好的方法是设置autouse=True,一劳永逸的避免漏加cleaned_db造成测试脏数据遗留的风险。

 1# -*- encoding: utf-8 -*-
 2import pytest
 3
 4@pytest.fixture(autouse=True)
 5def foo1():
 6    print("foo1")
 7
 8@pytest.fixture
 9def foo2():
10    print("foo2")
11
12def test_foo1():
13    assert True
14
15def test_foo1_2():
16    assert True

验证一下:可以看到foo1fixture没有显式引用,也被执行了;而foo2则没有执行。

注意:即使加了autouse=True之后,foo1依旧每个测试执行一次。另外,请不要误会:给fixture增加了autouse=True,不代表可以在任何测试方法里直接使用这个fixture。如果要使用这个fixture, 必须将其显式添加到待测方法的函数签名中。

1test.py::test_foo1_2
2foo1
3PASSED
4
5test.py::test_foo1
6foo1
7PASSED

使用scope则可以调整fixture执行的范围,如:

-scope默认值为function: 每个测试都执行一次 -class: 一个测试类只执行一次 -module: 一个.py文件执行一次 -packagetest.py所在包(当前包含__init__.py目录)及其子包(当前目录包含__init__.py的子目录)执行一次 -sessionpytest所执行的进程中,只执行一次

 1# -*- encoding: utf-8 -*-
 2import pytest
 3
 4@pytest.fixture(autouse=True)
 5def foo1():
 6    print("foo1")
 7
 8@pytest.fixture(autouse=True, scope="module")
 9def foo2():
10    print("foo2")
11
12def test_foo1():
13    assert True
14
15def test_foo1_2():
16    assert True

foo1scopefunction,故两个测试触发两次执行;foo2scopemodule,整个文件只执行一次。

同时注意:fixture会在scope指定范围内,第一个测试运行时,才会执行。

1test.py::test_foo1_2
2foo2
3foo1
4PASSED
5
6test.py::test_foo1
7foo1
8PASSED

yield

fixture可以用return或者yield返回对象给测试方法使用。如果使用yield则可以实现tearDown功能。例如:

 1# -*- encoding: utf-8 -*-
 2import pytest
 3
 4
 5@pytest.fixture
 6def foo1():
 7    print("foo1 start")
 8    yield "a"
 9    print("foo1 end")
10
11@pytest.fixture
12def foo2(foo1):
13    print("foo2 start")
14    yield "b"
15    raise ValueError("foo2")
16
17def test_foo1(foo2):
18    print("test_foo1")
19    assert True

test_foo1执行会调用foo2foo2调用foo1。当test_foo1执行完毕,则执行yield "b"之后的清理代码。即使遇到异常清理失败(ValueError),foo1yield "a"之后的清理代码依旧会被继续执行。

注意:test_foo1测试结果是PASSED,不会因为fixture清理失败而失败。

1test.py::test_foo1 
2foo1 start
3foo2 start
4test_foo1
5PASSED
6foo2 end    
7foo1 end
8
9test.py::test_foo1 ERROR