原文
总结
我们将从unittest
开始,尽管它并不那么好用,但它是Python标准库中的测试工具。
使用unittest
编写测试看起来像这样:
import unittest# 需要测试的代码
def add(a, b):return a + b# The tests
class TestAddFunction(unittest.TestCase):def setUp(self):... # This is run before each testdef tearDown(self):... # This is run after each testdef test_add_integers(self):result = add(1, 2)self.assertEqual(result, 3)def test_add_floats(self):result = add(0.1, 0.2)self.assertAlmostEqual(result, 0.3)def test_add_mixed_types(self):with self.assertRaises(TypeError):add(1, "2")if __name__ == "__main__":unittest.main()
当你运行测试时,你会得到一个关于测试是否通过的报告:
python the_tests.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000sOK
无聊警告
测试时一个难以教授的主题,因为你需要使用各种工具来减少测试的痛苦,但你不能直接就用工具。如果直接使用工具,你就无法理解为什么要那样编写测试。
为了后面能理解整个测试过程,我们需要先从一些无聊的概念开始。
Unittest
你可能听过单元测试(unit test)这个概念;另一方面,unittest
也是Python的标准库,但这个库不仅可以进行单元测试。
为了避免混淆,本文的unittest
表示Python的标准库。
我坦白:我不使用
unittest
编写测试,我们有更好的测试工具。但它毕竟是Python的标准库,可以作为我们的起点。
你的第一次测试
测试的环境,import会给初学者带来一些麻烦。因此,我们在本例中使用一个目录编写代码:
basic_project
├── the_code_to_test.py
└── the_tests.py
现在我们在the_code_to_test.py
编写未来可能价值千金的代码:
def add(a, b):return a + b
add as a service
接下来,我们将编写测试,保障我们代码的质量,在the_tests.py
中:
import unittestfrom the_code_to_test import addclass TestAddFunction(unittest.TestCase):def test_add_integers(self):result = add(1, 2)self.assertEqual(result, 3)if __name__ == "__main__":unittest.main()
现在我们可以运行测试了,在basic_project
目录下执行python the_tests.py
。
运行结果为:
python the_tests.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000sOK
发生神魔事了?
我们刚刚写了一个test_add_integers
的测试,该测试运行add(1,2)
并检查是否为3
。
使用unittest
执行该操作需要几个步骤:
#导入unittest
import unittest# 导入需要测试的代码
from the_code_to_test import add#继承TestCase.
#这对unittest很重要
class TestAddFunction(unittest.TestCase):#测试方法需要以"test"开头,以便unittest认出def test_add_integers(self):# 编写断言(assert),即我们期望的事情result = add(1, 2)self.assertEqual(result, 3)if __name__ == "__main__":unittest.main()
实际上,我们只关心其中的两句:
result = add(1, 2)
self.assertEqual(result, 3)
测试是不是很有意思呢?也许比洗碗更有意思!
测试也是必须的!
考虑我们的add函数中发生了一个错误:
def add(a, b):return a - b # woops
现在让我们看看测试报告:
python the_tests.py
F
======================================================================
FAIL: test_add_integers (__main__.TestAddFunction)
----------------------------------------------------------------------
Traceback (most recent call last):File "the_tests.py", line 9, in test_add_integersself.assertEqual(result, 3)
AssertionError: -1 != 3----------------------------------------------------------------------
Ran 1 test in 0.000sFAILED (failures=1)
测试报告其中一项测试未通过。我们稍后将看到如何解释这些结果。
进行多项测试
您不太可能只有一个测试,因此让我们添加更多测试。
首先,我们将add
函数恢复为合理的代码:
def add(a, b):return a + b
然后,让我们再添加一个断言来检查负数:
import unittestfrom the_code_to_test import addclass TestAddFunction(unittest.TestCase):def test_add_integers(self):result = add(1, 2)self.assertEqual(result, 3)result = add(1, -2)self.assertEqual(result, -1)if __name__ == "__main__":unittest.main()
我们的测试现在涵盖了更多用例。如果我们运行它,它仍然会报告1个测试。
python the_tests.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000sOK
一个测试(方法)确实可以包含多个断言,但它在报告中将作为一个整体成功或失败。
现在让我们添加一个新方法来测试添加字符串,因为 + 运算符也适用于字符串:
import unittestfrom the_code_to_test import addclass TestAddFunction(unittest.TestCase):def test_add_integers(self):result = add(1, 2)self.assertEqual(result, 3)result = add(1, -2)self.assertEqual(result, -1)def test_add_strings(self):result = add("1", "2")self.assertEqual(result, "12")if __name__ == "__main__":unittest.main()
运行它,我们可以看到2个测试:
python the_tests.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000sOK
让我们通过添加第 3 个测试来使其失败,以便您可以查看报告会发生什么。我们将尝试添加浮点数,这不能安全地与相等进行比较:
import unittestfrom the_code_to_test import addclass TestAddFunction(unittest.TestCase):def test_add_integers(self):result = add(1, 2)self.assertEqual(result, 3)result = add(1, -2)self.assertEqual(result, -1)def test_add_strings(self):result = add("1", "2")self.assertEqual(result, "12")def test_add_floats(self):result = add(0.1, 0.2)self.assertEqual(result, 0.3)if __name__ == "__main__":unittest.main()
现在运行测试可以得到:
python the_tests.py
F..
======================================================================
FAIL: test_add_floats (__main__.TestAddFunction)
----------------------------------------------------------------------
Traceback (most recent call last):File "the_tests.py", line 21, in test_add_floatsself.assertEqual(result, 0.3)
AssertionError: 0.30000000000000004 != 0.3----------------------------------------------------------------------
Ran 3 tests in 0.000sFAILED (failures=1)
我们现在检测并执行了 3 个测试。其中一个失败了,所以我们得到的不是顶部的 3 个点,而是“F…”。
但是,这种失败不是由于代码中的错误,而是因为我们错误地编写了测试。这是测试中令人讨厌的事情之一,你有更多的机会犯错误,在你更有经验之前,这将是令人沮丧的。
坚持下去,这是一种“熟能生巧”的情况。您需要有经验才能高效编写代码。并使用 ChatGPT,它很棒。
另外,我不会撒谎,我仍然经常与测试作斗争,只是比以前少得多。
阅读报告
测试失败时,您需要的第一个条件反射是阅读报告,因此让我们解释一下它包含的内容。
我将再添加一个失败的测试:
import unittestfrom the_code_to_test import addclass TestAddFunction(unittest.TestCase):def test_add_integers(self):result = add(1, 2)self.assertEqual(result, 3)result = add(1, -2)self.assertEqual(result, -1)def test_add_strings(self):result = add("1", "2")self.assertEqual(result, "12")def test_add_floats(self):result = add(0.1, 0.2)self.assertEqual(result, 0.3)def test_add_mixed_types(self):add(1, "2")if __name__ == "__main__":unittest.main()
现在报告将向我们显示 4 个测试,其中 2 个失败:
python the_tests.py
F.E.
======================================================================
ERROR: test_add_mixed_types (__main__.TestAddFunction)
----------------------------------------------------------------------
Traceback (most recent call last):File "the_tests.py", line 24, in test_add_mixed_typesadd(1, "2")File "the_code_to_test.py", line 2, in addreturn a + b
TypeError: unsupported operand type(s) for +: 'int' and 'str'======================================================================
FAIL: test_add_floats (__main__.TestAddFunction)
----------------------------------------------------------------------
Traceback (most recent call last):File "the_tests.py", line 21, in test_add_floatsself.assertEqual(result, 0.3)
AssertionError: 0.30000000000000004 != 0.3----------------------------------------------------------------------
Ran 4 tests in 0.001sFAILED (failures=1, errors=1)
让我们给所有这些贴上标签:
红色标记的部分是错误(Error)。该信息出现了3 次。一次在顶部摘要中的字母“E”,一次在运行过程中为每个错误(我们这里只有一个)带有标签“ERROR”,在最终统计信息的末尾有一个errors=1。
错误是由于代码崩溃而未成功的测试。
橙色标记的是失败(failure)。该信息也重复了 3 次。一次在顶部的摘要中,带有字母“F”,一次在运行期间每次出现时带有“FAIL”标签(我们这里只有一个),一次在最终统计信息的末尾。
失败是没有成功的测试,因为返回了一个断言 False ,这意味着你对代码如何工作的期望被证明是错误的。
顶部的绿点是已通过的测试,因此报告中不再提及它们。
在标签“ERROR”和“FAIL”旁边,您可以看到未成功的测试名称(紫色)。这样,您就可以找到问题发生的位置。
最后,对于每个问题,您都有一个常规的 Python 堆栈跟踪,即以“traceback”开头的蓝色块。在 ERROR 中,堆栈跟踪将显示代码的哪一部分爆炸了。在 FAIL 块中,堆栈跟踪将显示返回 False 的断言。
各种断言
assertEqual 不是我们唯一可以做出的断言,还有很多: assertIs , assertIn , assertWarns …
我们将利用它来修复我们的测试。
我们在test_add_floats
的“FAIL” 是因为我们做了一个 0.1 + 0.2 == 0.3 会返回 True 的假设。这与其说是我们代码中的错误,不如说是我们的测试不正确。
由于编程语言精度限制,0.1+0.2 的结果会离0.3有些偏差:
>>> 0.1 + 0.2
0.30000000000000004
我们不能在这个测试中使用相等,但幸运的是,unittest 模块附带 assertAlmostEqual 了它,可以让你检查这两个数字是否非常接近:
def test_add_floats(self):result = add(0.1, 0.2)self.assertAlmostEqual(result, 0.3)
对于 test_add_mixed_types ,错误是有确定的。我们希望代码在用户传递混合类型时抛出 TypeError
。因此,让我们重写测试,以考虑此处的异常实际上是成功的:
def test_add_mixed_types(self):# The test now passes if the function raises a TypeErrorwith self.assertRaises(TypeError):add(1, "2")
我们的测试套件越来越快乐:
python the_tests.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.000sOK
Setup and tear down 初始化和清理
有些测试组需要您在每次测试之前准备一些东西,例如数据库、文件或只是预先计算的东西。以及其他需要在每次测试后删除、清理或关闭某些内容的内容。
这可以通过在测试类上声明 setUp
和tearDown
方法来完成:
import unittestfrom the_code_to_test import addclass TestAddFunction(unittest.TestCase):def setUp(self):# Anything you attach to self here is available# in other testsprint('This is run before each test')def tearDown(self):print('This is run after each test')def test_add_integers(self):result = add(1, 2)self.assertEqual(result, 3)result = add(1, -2)self.assertEqual(result, -1)def test_add_strings(self):result = add("1", "2")self.assertEqual(result, "12")def test_add_floats(self):result = add(0.1, 0.2)self.assertAlmostEqual(result, 0.3)def test_add_mixed_types(self):with self.assertRaises(TypeError):add(1, "2")if __name__ == "__main__":unittest.main()
您可以看到这些方法被自动调用:
python the_tests.py
This is run before each test
This is run after each test
.This is run before each test
This is run after each test
.This is run before each test
This is run after each test
.This is run before each test
This is run after each test
.
----------------------------------------------------------------------
Ran 4 tests in 0.000sOK
使用test runner
我们有:
if __name__ == "__main__":unittest.main()
事实上,这不是强制性的。我们可以删除它,并改用测试运行程序(test runner )。
测试运行程序是根据某些规则(例如文件名、它们在目录树中的位置等)检测、收集和运行所有测试的程序。
Unittest 现在自带测试运行程序,因此我们也可以运行所有以这种方式调用它的测试:
python -m unittest the_tests.py
我们也会得到测试报告。
测试运行程序在目录上工作,而不仅仅是一个文件,因此一旦项目增长,您可能更喜欢使用一个目录。Python 中还有其他测试运行程序,例如 nose、pytest、tox、nox 等。它们甚至可以附带很多工具,正如我们将在 pytest 中看到的那样。
事实上,编写测试是一件苦差事,因此我们应该尽可能轻松地编写它们,否则这样做的动力会迅速下降。
Pytest 使编写测试变得不那么痛苦,并且具有许多功能,一旦您认真对待测试,您很快就会学会欣赏这些功能。
由于所有这些原因,我们不会在 unittest 上花费更多时间,在本系列的下一部分中,我们将继续使用 pytest。
此外,测试是生成式 AI 大放异彩的领域之一,不要犹豫,使用你最喜欢的LLM,无论是copilot、chatgpt、claude 还是其他什么,创建测试。
给他们测试的函数,并告诉他们为该函数编写一个测试。你经常需要修复一些东西,但这比手写要快得多,至少对于简单的问题来说是这样。大多数测试并没有那么复杂。