[译] Python 测试入门

本教程适用于使用Python编写了出色的应用程序但尚未编写任何测试的任何人。

使用Python进行测试是一个巨大的话题,并且可能会带来很多复杂性,但是并不需要很困难。您可以通过几个简单的步骤开始为您的应用程序创建简单的测试,然后从那里开始进行构建。

在本教程中,您将学习如何创建基本测试,执行测试以及在用户之前查找错误!您将了解可用于编写和执行测试,检查应用程序性能甚至查找安全问题的工具。

测试您的代码

有很多方法可以测试您的代码。在本教程中,您将从最基本的步骤中学习技术,并朝着高级方法学习。

自动化与手动测试

好消息是,您可能已经创建了一个未意识到的测试。还记得您第一次运行应用程序并使用它吗?您是否检查了功能并尝试使用它们?这称为探索性测试,是手动测试的一种形式。

探索性测试是无计划的测试形式。在探索性测试中,您只是在探索应用程序。

要拥有一套完整的手动测试,您要做的就是列出应用程序具有的所有功能,可以接受的不同输入类型以及预期结果。现在,每次更改代码时,都需要遍历该列表中的每个项目并进行检查。

听起来不那么有趣,不是吗?

这就是自动测试的用处。自动测试是通过脚本而不是人工执行测试计划(您要测试的应用程序部分,要测试它们的顺序以及预期的响应)的执行。Python已经提供了一组工具和库来帮助您为应用程序创建自动化测试。在本教程中,我们将探索那些工具和库。

单元测试与集成测试

测试领​​域不乏术语,现在您已经知道自动测试和手动测试之间的区别,现在该更深入了。

想想如何测试汽车的灯光。您将打开灯(称为测试步骤)并离开车外,或请朋友检查灯是否点亮(称为测试断言)。测试多个组件称为集成测试

考虑所有需要正确工作的事物,以使一个简单的任务获得正确的结果。这些组件就像您的应用程序的部分,您编写的所有类,函数和模块一样。

集成测试的主要挑战是,集成测试无法给出正确的结果。如果无法找出系统的哪一部分发生故障,很难诊断问题。如果灯不亮,则可能是灯泡坏了。电池没电了吗?交流发电机呢?汽车的计算机出故障了吗?

如果您有一辆豪华的现代汽车,它将在灯泡熄灭时告诉您。它使用单元测试的形式进行此操作。

单元测试是一种较小的测试,它检查单个组件是否以正确的方式运行。单元测试可帮助您隔离应用程序中损坏的内容并更快地对其进行修复。

您刚刚看过两种测试:

  1. 集成测试将检查应用程序中的组件是否相互配合。
  2. 单元测试检查应用程序中的一小部分。

您可以使用Python编写集成测试和单元测试。要为内置功能编写单元测试sum(),您需要sum()对照已知输出检查输出。

例如,下面是检查sum()数字(1, 2, 3)等于的方法6

1
>>> assert sum([1, 2, 3]) == 6, "Should be 6"

由于值正确,因此不会在REPL上输出任何内容。

如果来自的结果sum()不正确,则会失败并显示AssertionError和消息"Should be 6"。再次尝试使用错误值的断言语句以查看AssertionError

1
2
3
4
>>> assert sum([1, 1, 1]) == 6, "Should be 6"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError: Should be 6

在REPL中,您看到一个 AssertionError 抛出,因为的结果sum()不匹配6

与其在REPL上进行测试,不如将其放入一个名为的新Python文件中test_sum.py并再次执行:

1
2
3
4
5
6
def test_sum():
assert sum([1, 2, 3]) == 6, "Should be 6"

if __name__ == "__main__":
test_sum()
print("Everything passed")

现在,您已经编写了一个测试用例,一个断言和一个入口点(命令行)。您现在可以在命令行中执行此操作:

1
2
$ python test_sum.py
Everything passed

您可以看到成功的结果Everything passed

在Python中,将sum()任何iterable作为其第一个参数。您使用列表进行了测试。现在也用元组测试。test_sum_2.py使用以下代码创建一个名为的新文件:

1
2
3
4
5
6
7
8
9
10
def test_sum():
assert sum([1, 2, 3]) == 6, "Should be 6"

def test_sum_tuple():
assert sum((1, 2, 2)) == 6, "Should be 6"

if __name__ == "__main__":
test_sum()
test_sum_tuple()
print("Everything passed")

当执行时test_sum_2.py,脚本将给出错误,因为sum()of (1, 2, 2)5,不是6。脚本的结果为您提供错误消息,代码行和回溯:

1
2
3
4
5
6
7
$ python test_sum_2.py
Traceback (most recent call last):
File "test_sum_2.py", line 9, in <module>
test_sum_tuple()
File "test_sum_2.py", line 5, in test_sum_tuple
assert sum((1, 2, 2)) == 6, "Should be 6"
AssertionError: Should be 6

在这里,您可以看到代码中的错误如何在控制台上产生错误,并提供有关错误的位置和预期的结果的一些信息。

以这种方式编写测试对于一个简单的检查是可以的,但是如果多个测试失败了怎么办?这是测试运行程序的用处。测试运行程序是一个特殊的应用程序,用于运行测试,检查输出并为您提供调试和诊断测试及应用程序的工具。

选择测试运行工具

有许多可用于Python的测试运行器。Python标准库中内置的一个称为unittest。在本教程中,您将使用unittest测试用例和unittest测试运行器。的原理unittest很容易移植到其他框架。三种最受欢迎​​的测试运行工具是:

  • unittest
  • nose / nose2
  • pytest

为您的需求和经验水平选择最佳的测试运行工具很重要。

unittest

unittest从2.1版开始已内置到Python标准库中。您可能会在商业Python应用程序和开源项目中看到它。

unittest包含测试框架和测试运行器。unittest对编写和执行测试有一些重要要求。

unittest 要求:

  • 您将测试作为方法放入类中
  • 您在unittest.TestCase类中使用了一系列特殊的断言方法,而不是内置assert语句

要将先前的示例转换为unittest测试用例,您必须:

  1. unittest从标准库导入
  2. 创建一个TestSumTestCase该类继承的类
  3. 通过添加self第一个参数将测试函数转换为方法
  4. 更改断言以self.assertEqual()TestCase类上使用方法
  5. 将命令行入口点更改为call unittest.main()

按照以下步骤操作test_sum_unittest.py,使用以下代码创建一个新文件:

1
2
3
4
5
6
7
8
9
10
11
12
import unittest

class TestSum(unittest.TestCase):

def test_sum(self):
self.assertEqual(sum([1, 2, 3]), 6, "Should be 6")

def test_sum_tuple(self):
self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")

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

如果在命令行上执行此操作,则会看到一个成功(以.表示)和一个失败(以F表示):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python test_sum_unittest.py
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_sum_unittest.py", line 9, in test_sum_tuple
self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

您刚刚使用unittest测试运行器执行了两个测试。

注意:如果要编写需要同时在Python 2和3中执行的测试用例,请务必小心。在python 2.7及以下版本中,unittest称为unittest2。如果仅从中导入unittest,则将在Python 2和3之间获得具有不同功能的不同版本。

有关更多信息unittest,您可以浏览单元测试文档

nose

随着时间的流逝,您可能会发现,随着为应用程序编写数百甚至数千个测试,变得越来越难以理解和使用的输出unittest

nose与使用该unittest框架编写的任何测试兼容,并且可以用作unittest测试运行程序的直接替代品。nose作为开源应用程序的开发落后了,nose2因此创建了一个名为fork的分支。如果您是从头开始的,建议您使用nose2而不是nose

要开始使用nose2,请nose2从PyPI 安装并在命令行上执行。nose2将尝试在当前目录中发现所有命名的测试脚本test*.py和继承的测试用例unittest.TestCase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ pip install nose2
$ python -m nose2
.F
======================================================================
FAIL: test_sum_tuple (__main__.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_sum_unittest.py", line 9, in test_sum_tuple
self.assertEqual(sum((1, 2, 2)), 6, "Should be 6")
AssertionError: Should be 6

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

您刚刚执行了test_sum_unittest.pynose2测试运行程序创建的测试。nose2提供了许多命令行标志来过滤您执行的测试。有关更多信息,您可以浏览Nose 2文档

pytest

pytest支持unittest测试用例的执行。pytest编写pytest测试用例是其真正的优势。pytest测试用例是Python文件中名称开头的一系列函数test_

pytest 还有其他一些很棒的功能:

  • 支持内置assert语句,而不是使用特殊self.assert*()方法
  • 支持筛选测试用例
  • 能够从上次失败的测试中重新运行
  • 包含数百个插件的生态系统,以扩展功能

编写TestSum测试用例示例pytest将如下所示:

1
2
3
4
5
def test_sum():
assert sum([1, 2, 3]) == 6, "Should be 6"

def test_sum_tuple():
assert sum((1, 2, 2)) == 6, "Should be 6"

您已经删除了TestCase,对类的任何使用以及命令行入口点。

可以在Pytest文档网站上找到更多信息。

编写您的第一个测试

让我们将到目前为止所学的知识汇总起来,而不是测试内置sum()功能,而是测试相同需求的简单实现。

创建一个新的项目文件夹,并在其中创建一个名为的新文件夹my_sum。在内部my_sum,创建一个名为的空文件__init__.py。创建__init__.py文件意味着my_sum可以从父目录将文件夹作为模块导入。

您的项目文件夹应如下所示:

1
2
3

└── my_sum/
└── __init__.py

打开my_sum/__init__.py并创建一个名为的新函数sum(),该函数接受一个可迭代的(列表,元组或集合)并将这些值加在一起:

1
2
3
4
5
def sum(arg):
total = 0
for val in arg:
total += val
return total

此代码示例创建一个名为的变量total,遍历其中的所有值arg,并将它们添加到中total。一旦迭代结束,它将返回结果。

在哪里编写测试

要开始编写测试,您只需创建一个名为的文件test.py,其中将包含您的第一个测试用例。由于该文件将需要能够导入您的应用程序以进行测试,因此您希望将其放置test.py在package文件夹上方,因此目录树将如下所示:

1
2
3
4
5
6
project/

├── my_sum/
│ └── __init__.py
|
└── test.py

您会发现,随着添加越来越多的测试,单个文件将变得混乱并且难以维护,因此您可以创建一个名为的文件夹tests/并将测试拆分为多个文件。按照惯例,请确保每个文件均以test_ 开头,因此所有测试运行程序都将假定Python文件包含要执行的测试。一些非常大的项目根据测试的目的或用途将测试划分为更多的子目录。

注意:如果您的应用程序是单个脚本怎么办?

您可以使用内置函数 __import__() 导入脚本的任何属性,例如类,函数和变量。替换 from my_sum import sum,您可以编写以下代码:

1
2
target = __import__("my_sum.py")
sum = target.sum

使用 __import__() 的好处是您不必将项目文件夹变成一个包,您可以指定文件名。如果您的文件名与任何标准库包冲突,这也很有用。例如,math.py将与math模块碰撞。

如何构造一个简单的测试

在开始编写测试之前,您需要首先做出几个决定:

  1. 您要测试什么?
  2. 您正在编写单元测试还是集成测试?

然后,测试的结构应大致遵循以下工作流程:

  1. 创建您的输入
  2. 执行被测试的代码,捕获输出
  3. 将输出与预期结果进行比较

对于此应用程序,您正在测试sum()sum()您可以检查许多行为,例如:

  • 可以对整数(整数)列表求和吗?
  • 可以对一个元组或集合求和吗?
  • 可以汇总一个浮动列表吗?
  • 为它提供错误的值(例如单个整数或字符串)时会发生什么?
  • 如果其中一个值为负,会发生什么?

最简单的测试是整数列表。test.py使用以下Python代码创建文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import unittest

from my_sum import sum

class TestSum(unittest.TestCase):
def test_list_int(self):
"""
Test that it can sum a list of integers
"""
data = [1, 2, 3]
result = sum(data)
self.assertEqual(result, 6)

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

此代码示例:

  1. sum()my_sum您创建的包中导入
  2. 定义一个名为的新测试用例类TestSum,该类继承自unittest.TestCase
  3. 定义一种测试方法,.test_list_int()以测试整数列表。该方法.test_list_int()将:
    • data用数字列表声明一个变量(1, 2, 3)
    • 将的结果分配my_sum.sum(data)result变量
    • unittest.TestCase 类里的 .assertEqual() 方法断言结果值等于
  4. 定义一个命令行入口点,该入口点运行unittest测试运行器.main()

如果不确定定义什么self或如何.assertEqual()定义,可以使用Python 3面向对象编程复习面向对象的编程

如何写断言

编写测试的最后一步是根据已知响应验证输出。这称为断言。关于如何编写断言,有一些通用的最佳实践:

  • 确保测试可重复,并多次运行测试以确保每次给出的结果相同
  • 尝试并声明与输入数据有关的结果,例如检查sum()示例中结果是否为实际值的总和

unittest附带了许多方法来声明变量的值,类型和存在性。以下是一些最常用的方法:

方法 相当于
.assertEqual(a, b) a == b
.assertTrue(x) bool(x) is True
.assertFalse(x) bool(x) is False
.assertIs(a, b) a is b
.assertIsNone(x) x is None
.assertIn(a, b) a in b
.assertIsInstance(a, b) isinstance(a, b)

.assertIs().assertIsNone().assertIn(),和.assertIsInstance()所有有相反的方法,命名.assertIsNot(),等等。

副作用

在编写测试时,通常不像查看函数的返回值那么简单。通常,执行一段代码会更改环境中的其他内容,例如类的属性,文件系统上的文件或数据库中的值。这些被称为副作用,是测试的重要组成部分。在将其包含在断言列表之前,请确定是否正在测试副作用。

如果发现要测试的代码单元有很多副作用,则可能违反了“ 单一职责原则”。违反单一责任原则意味着代码段执行了太多的工作,最好进行重构。遵循“单一职责原则”是一种设计代码的好方法,该代码很容易为最终的可靠应用编写可重复且简单的单元测试。

执行您的第一个测试

现在您已经创建了第一个测试,您想要执行它。当然,您知道它将通过,但是在创建更复杂的测试之前,应检查是否可以成功执行测试。

执行测试运行器

执行测试代码,检查断言并在控制台中提供测试结果的Python应用程序称为测试运行器

在的底部test.py,添加了以下代码片段:

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

这是命令行入口点。这意味着,如果您通过python test.py在命令行上运行单独执行脚本,它将调用unittest.main()。这将通过发现此文件中继承自的所有类来执行测试运行程序unittest.TestCase

这是执行unittest测试运行程序的多种方法之一。当您有一个名为的测试文件时test.py,调用python test.py是入门的好方法。

另一种方法是使用unittest命令行。尝试这个:

1
$ python -m unittest test

这将test通过命令行执行相同的测试模块(称为)。

您可以提供其他选项来更改输出。其中之一是-v冗长的。接下来尝试:

1
2
3
4
5
$ python -m unittest -v test
test_list_int (test.TestSum) ... ok

----------------------------------------------------------------------
Ran 1 tests in 0.000s

这在内部执行了一个测试test.py,并将结果打印到控制台。详细模式列出了它首先执行的测试的名称以及每个测试的结果。

除了提供包含测试的模块的名称之外,您还可以使用以下方法请求自动发现:

1
$ python -m unittest discover

这将在当前目录中搜索任何已命名的文件,test*.py并尝试对其进行测试。

一旦有了多个测试文件,只要遵循test*.py命名模式,就可以提供目录名称,而不必使用-s标志和目录名称:

1
$ python -m unittest discover -s tests

unittest 将在一个测试计划中运行所有测试,并为您提供结果。

最后,如果您的源代码不在目录根目录中,而是包含在子目录中,例如在名为的文件夹中src/,则可以告诉unittest执行测试的位置,以便可以使用-t标志正确导入模块:

1
$ python -m unittest discover -s tests -t src

unittest将更改为src/目录,扫描目录中的所有test*.py文件tests,然后执行它们。

了解测试输出

那是一个非常简单的示例,一切都通过了,因此现在您将尝试失败的测试并解释输出。

sum() 应该能够接受其他数字类型列表,例如小数。

test.py文件顶部,添加导入语句以Fractionfractions标准库中的模块导入类型:

1
from fractions import Fraction

现在添加一个带有断言的测试,该断言期望值不正确,在这种情况下,期望1 / 4、1 / 4和2/5之和为1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import unittest

from my_sum import sum


class TestSum(unittest.TestCase):
def test_list_int(self):
"""
Test that it can sum a list of integers
"""
data = [1, 2, 3]
result = sum(data)
self.assertEqual(result, 6)

def test_list_fraction(self):
"""
Test that it can sum a list of fractions
"""
data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)]
result = sum(data)
self.assertEqual(result, 1)

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

如果使用再次执行测试python -m unittest test,则应该看到以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python -m unittest test
F.
======================================================================
FAIL: test_list_fraction (test.TestSum)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 21, in test_list_fraction
self.assertEqual(result, 1)
AssertionError: Fraction(9, 10) != 1

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

在输出中,您将看到以下信息:

  1. 第一行显示了所有测试的执行结果,其中一个失败(F)和一个通过(.)。

  2. FAIL条目显示有关失败测试的一些详细信息:

    • 测试方法名称(test_list_fraction
    • 测试模块(test)和测试用例(TestSum
    • 追溯到失败的行
    • 带有预期结果(1)和实际结果(Fraction(9, 10))的断言的详细信息

请记住,可以通过将-v标志添加到python -m unittest命令来向测试输出添加额外的信息。

从PyCharm运行测试

如果您使用的是PyCharm IDE,则可以运行unittestpytest按照以下步骤操作:

  1. 在项目工具窗口中,选择tests目录。
  2. 在上下文菜单上,选择的运行命令unittest。例如,选择 Run ‘Unittests in my Tests…’.

这将unittest在测试窗口中执行,并在PyCharm中为您提供结果:

有关更多信息,请访问PyCharm网站

从Visual Studio代码运行测试

如果你正在使用微软的Visual Studio IDE的代码,支持unittestnosepytest执行内置于Python的插件。

如果安装了Python插件,则可以通过使用 Ctrl+ Shift+ P 打开命令面板并键入 “Python测试” 来设置测试的配置。您将看到一系列选项:

选择Debug All Unit Tests,然后VSCode会提示您配置测试框架。单击齿轮以选择测试运行器(unittest)和主目录(.)。

设置完成后,您将在窗口底部看到测试状态,并且可以通过单击以下图标快速访问测试日志并再次运行测试:

这表明测试正在执行,但是其中一些失败。

测试Django和Flask等Web框架

如果您使用Django或Flask等流行的框架之一为Web应用程序编写测试,则编写和运行测试的方式会有一些重要差异。

为什么它们与其他应用程序不同

考虑一下您将在Web应用程序中测试的所有代码。路线,视图和模型都需要大量导入,并且需要有关所使用框架的知识。

这类似于本教程开始时的汽车测试:您必须启动汽车的计算机,然后才能运行简单的测试,例如检查车灯。

Django和Flask通过提供基于的测试框架,使您轻松实现了这一目标unittest。您可以按照学习的方式继续编写测试,但执行方式略有不同。

如何使用Django Test Runner

Django startapp模板将tests.py在您的应用程序目录中创建一个文件。如果还没有,则可以使用以下内容创建它:

1
2
3
4
from django.test import TestCase

class MyTestCase(TestCase):
# Your test methods

到目前为止,这些示例的主要区别在于您需要继承django.test.TestCase而不是unittest.TestCase。这些类具有相同的API,但是Django TestCase类设置了所有必需的状态以进行测试。

要执行测试套件unittest,请使用manage.py test

1
$ python manage.py test

如果您需要多个测试文件,请tests.py用文件夹替换,在文件夹中tests插入一个空文件__init__.py,然后创建您的test_*.py文件。Django将发现并执行这些。

有关更多信息,请访问Django文档网站

如何使用unittestFlask

Flask 要求导入该应用,然后将其设置为测试模式。您可以实例化一个测试客户端,并使用该测试客户端向应用程序中的任何路由发出请求。

所有测试客户端实例化都是通过setUp测试用例的方法完成的。在以下示例中,my_app是应用程序的名称。如果您不知道该怎么做,请不要担心setUp。您将在“ 更高级的测试方案”部分中了解到这一点。

测试文件中的代码应如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
import my_app
import unittest

class MyTestCase(unittest.TestCase):

def setUp(self):
my_app.app.testing = True
self.app = my_app.app.test_client()

def test_home(self):
result = self.app.get('/')
# Make your assertions

然后,您可以使用python -m unittest discover命令执行测试用例。

有关更多信息,请参见Flask文档网站

更多高级测试方案

在开始为应用程序创建测试之前,请记住每个测试的三个基本步骤:

  1. 创建您的输入
  2. 执行代码,捕获输出
  3. 将输出与预期结果进行比较

这并不总是像为字符串或数字之类的输入创建静态值那样容易。有时,您的应用程序将需要一个类或上下文的实例。那你怎么办呢?

您创建为输入的数据称为夹具。创建固定装置并重复使用它们是常见的做法。

如果您正在运行相同的测试,并且每次都传递不同的值并期望得到相同的结果,则称为参数化

处理预期的故障

之前,当您列出要测试的方案列表时sum(),出现了一个问题:如果为它提供错误的值(例如单个整数或字符串),会发生什么?

在这种情况下,您可能会sum()引发错误。当它确实引发错误时,将导致测试失败。

有一种特殊的方法可以处理预期的错误。您可以.assertRaises()用作上下文管理器,然后在with块内执行测试步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import unittest

from my_sum import sum


class TestSum(unittest.TestCase):
def test_list_int(self):
"""
Test that it can sum a list of integers
"""
data = [1, 2, 3]
result = sum(data)
self.assertEqual(result, 6)

def test_list_fraction(self):
"""
Test that it can sum a list of fractions
"""
data = [Fraction(1, 4), Fraction(1, 4), Fraction(2, 5)]
result = sum(data)
self.assertEqual(result, 1)

def test_bad_type(self):
data = "banana"
with self.assertRaises(TypeError):
result = sum(data)

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

现在,只有sum(data)提高时,此测试用例才能通过TypeError。您可以TypeError用任何选择的异常类型替换。

隔离应用程序中的行为

在本教程的前面,您了解了副作用。副作用使单元测试更加困难,因为每次运行测试时,它可能会给出不同的结果,甚至更糟的是,一个测试可能会影响应用程序的状态并导致另一个测试失败!

您可以使用一些简单的技术来测试应用程序的某些部分,这些部分有很多副作用:

  • 重构代码以遵循单一职责原则
  • 模拟出任何方法或函数调用以消除副作用
  • 在此应用程序中使用集成测试代替单元测试

如果您不熟悉模拟,请参阅Python CLI Testing以获取一些出色的示例。

编写集成测试

到目前为止,您一直在主要学习有关单元测试的知识。单元测试是构建可预测且稳定的代码的好方法。但是,归根结底,您的应用程序需要在启动时才能运行!

集成测试是对应用程序的多个组件的测试,以检查它们是否可以协同工作。集成测试可能需要通过以下方式像应用程序的使用者或用户一样行动:

  • 调用HTTP REST API
  • 调用Python API
  • 调用网络服务
  • 运行命令行

可以按照输入,执行和断言模式,以与单元测试相同的方式编写所有这些类型的集成测试。最大的不同是集成测试一次检查了更多的组件,因此比单元测试有更多的副作用。此外,集成测试将需要更多的固定装置,例如数据库,网络套接字或配置文件。

这就是为什么最好将单元测试和集成测试分开的原因。创建集成所需的固定装置(例如测试数据库)和测试用例本身通常比单元测试花费更多的时间,因此您可能只想在推送到生产环境之前运行集成测试,而不是每次提交都运行一次。

分离单元测试和集成测试的简单方法是将它们放在不同的文件夹中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
project/

├── my_app/
│ └── __init__.py

└── tests/
|
├── unit/
| ├── __init__.py
| └── test_sum.py
|
└── integration/
├── __init__.py
└── test_integration.py

有很多方法可以仅执行一组选定的测试。指定源目录标志-s可以unittest discover与包含测试的路径一起添加:

1
$ python -m unittest discover -s tests/integration

unittest将为您提供tests/integration目录中所有测试的结果。

测试数据驱动的应用程序

许多集成测试将要求后端数据(如数据库)必须具有某些值。例如,您可能需要进行一项测试,以检查应用程序是否正确显示了数据库中的100个以上客户,或者即使产品名称以日语显示,订单页面也可以正常工作。

这些类型的集成测试将取决于不同的测试装置,以确保它们可重复且可预测。

使用的一种好方法是将测试数据存储在集成测试文件夹中的一个文件夹中,该文件夹称为fixtures,指示其中包含测试数据。然后,在测试中,您可以加载数据并运行测试。

如果数据由JSON文件组成,则下面是该结构的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
project/

├── my_app/
│ └── __init__.py

└── tests/
|
└── unit/
| ├── __init__.py
| └── test_sum.py
|
└── integration/
|
├── fixtures/
| ├── test_basic.json
| └── test_complex.json
|
├── __init__.py
└── test_integration.py

在您的测试用例中,您可以使用该.setUp()方法从已知路径中的夹具文件中加载测试数据,并针对该测试数据执行许多测试。请记住,您可以在一个Python文件中包含多个测试用例,并且unittest发现将同时执行。每个测试数据集可以有一个测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import unittest

class TestBasic(unittest.TestCase):
def setUp(self):
# Load test data
self.app = App(database='fixtures/test_basic.json')

def test_customer_count(self):
self.assertEqual(len(self.app.customers), 100)

def test_existence_of_customer(self):
customer = self.app.get_customer(id=10)
self.assertEqual(customer.name, "Org XYZ")
self.assertEqual(customer.address, "10 Red Road, Reading")

class TestComplexData(unittest.TestCase):
def setUp(self):
# load test data
self.app = App(database='fixtures/test_complex.json')

def test_customer_count(self):
self.assertEqual(len(self.app.customers), 10000)

def test_existence_of_customer(self):
customer = self.app.get_customer(id=9999)
self.assertEqual(customer.name, u"バナナ")
self.assertEqual(customer.address, "10 Red Road, Akihabara, Tokyo")

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

如果您的应用程序依赖于远程位置(例如远程API)的数据,则需要确保测试可重复。由于API离线或存在连接问题而导致测试失败,可能会减慢开发速度。在这些类型的情况下,最佳实践是在本地存储远程设备,以便可以调用它们并将其发送到应用程序。

requests库有一个名为的免费软件包responses,使您可以创建响应固定装置并将其保存在测试文件夹中。在其GitHub Page上找到更多信息

在多种环境中测试

到目前为止,您已经在使用带有一组特定依赖项的虚拟环境针对Python的单个版本进行了测试。您可能要检查您的应用程序是否可以在多个版本的Python或程序包的多个版本上运行。Tox是在多个环境中自动化测试的应用程序。

安装毒物

Tox可以通过以下方式在PyPI上安装pip

1
$ pip install tox

现在您已经安装了Tox,需要对其进行配置。

为您的依赖配置Tox

Tox通过项目目录中的配置文件进行配置。Tox配置文件包含以下内容:

  • 为了执行测试而运行的命令
  • 执行之前需要的任何其他软件包
  • 要测试的目标Python版本

您无需学习Tox配置语法,而可以通过运行quickstart应用程序来抢先一步:

1
$ tox-quickstart

Tox配置工具将询问您这些问题,并在中创建类似于以下内容的文件tox.ini

1
2
3
4
5
6
7
8
[tox]
envlist = py27, py36

[testenv]
deps =

commands =
python -m unittest discover

在运行Tox之前,它要求setup.py您的应用程序文件夹中包含一个文件,其中包含安装软件包的步骤。如果您还没有,则可以在继续之前按照本指南操作如何创建setup.py

或者,如果您的项目不适合在PyPI上分发,则可以通过tox.ini[tox]标题下的文件中添加以下行来跳过此要求:

1
2
3
[tox]
envlist = py27, py36
skipsdist=True

如果您不创建setup.py,并且您的应用程序具有PyPI的某些依赖项,则需要在本[testenv]节下的多行中指定这些依赖项。例如,Django需要具备以下条件:

1
2
[testenv]
deps = django

一旦完成该阶段,就可以运行测试了。

现在,您可以执行Tox,它将创建两个虚拟环境:一个用于Python 2.7,一个用于Python 3.6。Tox目录称为.tox/。在.tox/目录内,Tox将python -m unittest discover针对每个虚拟环境执行。

您可以通过在命令行上调用Tox来运行此过程:

1
$ tox

Tox将输出针对每种环境的测试结果。Tox第一次运行时,会花一点时间来创建虚拟环境,但是一旦运行起来,第二次执行就会快很多。

执行毒药

Tox的输出非常简单。它为每个版本创建一个环境,安装您的依赖项,然后运行测试命令。

还有一些其他值得记住的命令行选项。

仅运行单个环境,例如Python 3.6:

1
$ tox -e py36

重新创建虚拟环境,以防您的依存关系已更改或站点包损坏:

1
$ tox -r

使用更少的详细输出运行Tox:

1
$ tox -q

使用更详细的输出运行Tox:

1
$ tox -v

可以在Tox文档网站上找到有关Tox的更多信息。

自动化执行测试

到目前为止,您已经通过运行命令手动执行测试。有一些工具可以在您进行更改并将其提交到诸如Git之类的源代码控制存储库时自动执行测试。自动化测试工具通常被称为CI / CD工具,代表“持续集成/持续部署”。他们可以运行您的测试,编译和发布任何应用程序,甚至将它们部署到生产环境中。

Travis CI是许多可用的CI(连续集成)服务之一。

Travis CI与Python配合良好,现在您已经创建了所有这些测试,您可以在云中自动执行它们!Travis CI对于GitHub和GitLab上的任何开源项目都是免费的,并且对私人项目收费。

首先,请登录网站并使用Gi​​tHub或GitLab凭据进行身份验证。然后创建一个.travis.yml具有以下内容的文件:

1
2
3
4
5
6
7
8
language: python
python:
- "2.7"
- "3.7"
install:
- pip install -r requirements.txt
script:
- python -m unittest discover

此配置指示Travis CI:

  1. 针对Python 2.7和3.7进行测试(您可以使用任意版本替换这些版本。)
  2. 安装列出的所有软件包requirements.txt(如果没有任何依赖性,则应删除此部分。)
  3. 运行python -m unittest discover以运行测试

提交并推送此文件后,每次您推送到远程Git存储库时,Travis CI都会运行这些命令。您可以在他们的网站上查看结果。

下一步是什么

既然您已经了解了如何创建测试,执行测试,将其包含在项目中,甚至自动执行它们,随着测试库的增长,您可能会发现一些方便的高级技术。

将Linters引入您的应用程序

Tox和Travis CI具有测试命令的配置。在本教程中一直使用的测试命令是python -m unittest discover

您可以在所有这些工具中提供一个或多个命令,并且该选项使您可以添加更多工具来提高应用程序的质量。

一种这样的应用程序称为短绒。短毛猫将查看您的代码并对其进行注释。它可以为您提供有关您犯的错误,纠正尾随空格的提示,甚至可以预测您可能引入的错误。

有关短毛绒的更多信息,请阅读Python代码质量教程

被动掉毛 flake8

一个流行的linter可以评论与PEP 8规范有关的代码样式flake8

您可以flake8使用安装pip

1
$ pip install flake8

然后,您可以flake8在单个文件,文件夹或模式上运行:

1
2
3
4
$ flake8 test.py
test.py:6:1: E302 expected 2 blank lines, found 1
test.py:23:1: E305 expected 2 blank lines after class or function definition, found 1
test.py:24:20: W292 no newline at end of file

您将看到flake8已找到的代码的错误和警告的列表。

flake8 可在命令行中或项目中的配置文件中进行配置。如果您想忽略某些规则(E305如上所示),则可以在配置中进行设置。flake8将检查.flake8项目文件夹中的文件或setup.cfg文件。如果您决定使用Tox,则可以将flake8配置部分放在里面tox.ini

本示例忽略.git__pycache__目录以及E305规则。此外,它将最大行长设置为90,而不是80个字符。您可能会发现,默认的线宽限制是79个字符,这对于测试来说是非常有限的,因为它们包含长的方法名称,带有测试值的字符串文字以及可能更长的其他数据。通常将测试的行长设置为最多120个字符:

1
2
3
4
[flake8]
ignore = E305
exclude = .git,__pycache__
max-line-length = 90

或者,您可以在命令行上提供以下选项:

1
$ flake8 --ignore E305 --exclude .git,__pycache__ --max-line-length=90

文档网站上提供了配置选项的完整列表。

现在,您可以添加flake8到CI配置。对于Travis CI,其外观如下:

1
2
3
4
matrix:
include:
- python: "2.7"
script: "flake8"

Travis将在其中读取配置,.flake8如果发生任何棉绒错误,将使构建失败。确保将flake8依赖项添加到requirements.txt文件中。

使用代码格式化程序进行进取性整理

flake8是一个被动的lint:它建议更改,但是您必须去更改代码。更具攻击性的方法是代码格式化程序。代码格式化程序将自动更改您的代码,以满足一系列样式和布局惯例。

black是一个非常宽容的格式化程序。它没有任何配置选项,并且具有非常特定的样式。这非常适合作为插入测试管道的插入工具。

注意: black需要Python 3.6+。

您可以black通过pip 安装:

1
$ pip install black

然后,要black在命令行上运行,请提供要格式化的文件或目录:

1
$ black test.py

保持测试代码干净

编写测试时,您可能会发现复制和粘贴代码的结果比常规应用程序要多得多。测试有时会非常重复,但这绝不是让代码草率且难以维护的原因。

随着时间的流逝,您将在测试代​​码中积累许多技术负担,并且如果您对应用程序进行了重大更改,需要对测试进行更改,那么由于结构化方式,这可能是不必要的繁琐任务。

尝试按照DRY编写测试时的原则:d on't [R EPEAT ÿ我们自己。

测试装置和功能是产生易于维护的测试代码的好方法。另外,可读性也很重要。考虑像flake8测试代码一样部署一个整理工具:

1
$ flake8 --max-line-length=120 tests/

测试变更之间的性能下降

有许多方法可以在Python中对代码进行基准测试。标准库提供了该timeit模块,该模块可以多次计时功能,并为您提供分配。此示例将执行test()100次,并print()输出:

1
2
3
4
5
6
def test():
# ... your code

if __name__ == '__main__':
import timeit
print(timeit.timeit("test()", setup="from __main__ import test", number=100))

如果您决定pytest用作测试运行程序,则另一个选择是pytest-benchmark插件。这提供了一个pytest称为的装置benchmark。您可以传递benchmark()任何可调用对象,它将可调用对象的时间记录到的结果中pytest

您可以pytest-benchmark使用pip以下方法从PyPI 安装:

1
$ pip install pytest-benchmark

然后,您可以添加一个使用夹具的测试,并通过要执行的可调用对象:

1
2
def test_my_function(benchmark):
result = benchmark(test)

pytest现在执行将为您提供基准测试结果:

有关更多信息,请访问文档网站

测试应用程序中的安全缺陷

您将要在应用程序上运行的另一项测试是检查常见的安全错误或漏洞。

您可以bandit使用pip以下方法从PyPI 安装:

1
$ pip install bandit

然后,您可以通过-r标志传递应用程序模块的名称,它会为您提供一个摘要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ bandit -r my_sum
[main] INFO profile include tests: None
[main] INFO profile exclude tests: None
[main] INFO cli include tests: None
[main] INFO cli exclude tests: None
[main] INFO running on Python 3.5.2
Run started:2018-10-08 00:35:02.669550

Test results:
No issues identified.

Code scanned:
Total lines of code: 5
Total lines skipped (#nosec): 0

Run metrics:
Total issues (by severity):
Undefined: 0.0
Low: 0.0
Medium: 0.0
High: 0.0
Total issues (by confidence):
Undefined: 0.0
Low: 0.0
Medium: 0.0
High: 0.0
Files skipped (0):

与一样flake8bandit标记的规则是可配置的,并且如果您希望忽略任何标记,则可以使用以下setup.cfg选项将以下部分添加到文件中:

1
2
3
[bandit]
exclude: /test
tests: B101,B102,B301

可以在GitHub网站上获得更多详细信息。

结论

Python通过内置所需的命令和库来验证应用程序是否按设计工作,从而使测试变得可访问。使用Python进行测试的入门不必太复杂:您可以使用unittest和编写小型可维护的方法来验证代码。

随着您了解有关测试的更多信息以及您的应用程序的增长,您可以考虑切换到其他测试框架之一,例如pytest,并开始利用更多高级功能。

感谢您的阅读。希望您在使用Python时拥有无错误的未来!

原文:Getting Started With Testing in Python
作者:Anthony Shaw