python中的单元测试
Catch bugs before you even write them
甚至在编写错误之前就将其捕获
Python is a multi-purpose language that is used for everything backend. In this article, I will teach you to perform basic unit testing in Python, how to mock modules, and make sure your code is clean.
Python是一种用于所有后端的多用途语言。 在本文中,我将教您使用Python执行基本的单元测试,如何模拟模块并确保代码干净。
什么是单元测试? (What is Unit-testing?)
Unit-testing is one of the ways to test your code. Other ways include functional testing, integration testing, regression testing and so on. Testing is vital to any larger codebase, as it lets you iterate and perform changes quickly, without worrying to much about what is going to break.
单元测试是测试代码的方法之一。 其他方式包括功能测试,集成测试,回归测试等等。 测试对于任何更大的代码库都是至关重要的,因为它使您可以快速迭代并执行更改,而不必担心会发生什么。
Unit-testing is the lowest testing method on the abstraction level. Unit-testing is concerned with testing individual modules and functions in isolation. That is, by making sure all the parts of your system work correctly, you can make the assumption that the whole system is working ok.
单元测试是抽象级别上最低的测试方法。 单元测试是关于隔离测试单独的模块和功能。 也就是说,通过确保系统的所有部分正常工作,您可以假设整个系统都可以正常工作。
That is, in ideal world, of course. While unit-testing is very valuable, the whole system is not merely a sum of its parts: even if every function is working as intended, you will still need to test how well do they fit together (which is out of scope of this article).
就是说,在理想世界中。 尽管单元测试非常有价值,但是整个系统不仅仅是其各个部分的总和:即使每个功能都按预期工作,您仍然需要测试它们之间的配合程度(这超出了本文的范围) )。
单元测试与TDD有何关系? (How does Unit-testing relate to TDD?)
Unit-testing is one of the foundations of TDD. TDD stands for Test Driven Development, and is a methodology for producing quality software. In essence, it all boils down to these:
单元测试是TDD的基础之一。 TDD代表“测试驱动开发”,是一种生产高质量软件的方法。 从本质上讲,这全都归结为以下几点:
Write tests first. Think about how different parts of your system work in isolation and write tests that validate their intended behaviour. Your tests must fail, because you have not written any actual code yet.
首先编写测试。 考虑一下系统的不同部分如何隔离工作,并编写验证其预期行为的测试。 测试必须失败,因为您尚未编写任何实际代码。
- Write just enough code to make the tests pass. 编写足够的代码以使测试通过。
- Refactor what you just wrote. 重构您刚刚写的内容。
- Go to step 1. 转到步骤1。
That is it! The whole cycle takes a couple of minutes, but this simple technique will make sure your system is (1) working according to the specs and (2) is testable. But to get started with TDD, you need to master basic unit-testing first.
这就对了! 整个周期需要花费几分钟,但是这种简单的技术将确保您的系统(1)根据规范工作,并且(2)是可测试的。 但是要开始使用TDD,您需要首先掌握基本的单元测试。
unittest
模块 (unittest
module)
Unit-testing in Python is available out of the box with the unittest
module. We will learn unit-testing by developing our own factorial function. To get started, open a new Python file add write this code it it:
Python中的单元测试可与unittest
模块一起使用。 我们将通过开发我们自己的阶乘函数来学习单元测试。 首先,打开一个新的Python文件,添加以下代码:
import unittest
class MyTestCase(unittest.TestCase):
def test_something(self):
self.assertEqual(True, False)
if __name__ == '__main__':
unittest.main()
On line 1 you can notice the imported unittest
module. On lines 4-6 we define a test case by extending the TestCase
class. In it, we only have 1 test at the moment, the test_something
. It is important that tests are prefixed with test_
, so that the test runner can find them. Finally, on lines 9-10 we execute the tests. If you try running this file now, the test will fail:
在第1行上,您可以注意到导入的unittest
模块。 在第4-6行,我们通过扩展TestCase
类来定义测试用例。 其中,我们目前只有1个测试,即test_something
。 重要的是,测试必须以test_
作为前缀,以便测试运行器可以找到它们。 最后,在9-10行,我们执行测试。 如果尝试立即运行此文件,则测试将失败:
======================================================================
FAIL: test_something (__main__.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "main.py", line 6, in test_something
self.assertEqual(True, False)
AssertionError: True != False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
That is happening because on line 6 we are asserting that True
and False
are equal, which is obviously false. Let's change that to test our (unwritten!) function:
之所以会这样,是因为在第6行中,我们断言True
和False
相等,这显然是错误的。 让我们更改它以测试我们的(未编写!)功能:
class FactorialTestCase(unittest.TestCase):
def test_factorial(self):
result = factorial(3)
self.assertEqual(result, 6)
If you run this now, it will still fail, because we did not write the factorial
function yet. Let's make this test pass:
如果您现在运行此命令,它将仍然会失败,因为我们尚未编写factorial
函数。 让我们通过这个测试:
def factorial(num):
return 6
While this is not mathematically sound, this makes our test pass. Let’s write some more tests:
尽管这在数学上并不合理,但这使我们的测试通过了。 让我们再写一些测试:
def test_factorial(self):
self.assertEqual(factorial(1), 1)
self.assertEqual(factorial(2), 2)
self.assertEqual(factorial(3), 6)
self.assertEqual(factorial(10), 3628800)
Check that the tests now fail (since factorial
returns 6 all the time) and make the changes to the factorial function:
检查测试现在是否失败(因为factorial
始终返回6),并对阶乘函数进行更改:
def factorial(num):
if num == 1:
return 1
return num * factorial(num - 1)
And, if you run this, the tests pass:
而且,如果运行此命令,则测试通过:
Ran 1 test in 0.002s OK
Now, let’s take a step back and understand what we just did.
现在,让我们退后一步,了解我们刚才所做的事情。
断言 (Assertions)
When we write tests, we want to test for something. If something is equal, or not equal, if a function throws an exception, if a method was called with certain parameters, etc. Such checks are called assertions. Assertions are things that are always supposed to be true for the code to work properly.
编写测试时,我们想测试一些东西。 如果相等或不相等,则函数抛出异常,使用特定参数调用方法等。此类检查称为断言。 断言是为了使代码正常工作而总是应该做的事情。
One assertion you just saw if the assertEqual
. It checks that 2 variables passed in are equal to each other. assertEqual
is available through self
, and is provided by the TestCase
parent class. Here are some of the common assertions you will find useful:
您刚刚看到了一个断言是否为assertEqual
。 它检查传入的2个变量是否相等。 assertEqual
通过self
可用,由TestCase
父类提供。 以下是一些有用的常见断言:
assertEqual(x, y)/assertNotEqual(x, y)
assertEqual(x, y)/assertNotEqual(x, y)
assertTrue(x)/assertFalse(x)
assertTrue(x)/assertFalse(x)
assertIs(x, y)
assertIs(x, y)
assertIsNone(x)
assertIsNone(x)
assertIn(x, y)
assertIn(x, y)
assertIsInstance(x, y)/assertNotIsInstance(x, y)
assertIsInstance(x, y)/assertNotIsInstance(x, y)
assertRaises(exc, fun, *args, **kwargs)
assertRaises(exc, fun, *args, **kwargs)
assertGreater(x, y)/assertLess(x, y)/assertGreaterEqual(x, y)/assertLessEqual(x, y)
assertGreater(x, y)/assertLess(x, y)/assertGreaterEqual(x, y)/assertLessEqual(x, y)
Some of these are available using a context manager, like assertRaises
. It makes the code a bit easier to read:
其中一些可以使用上下文管理器来使用,例如assertRaises
。 它使代码更易于阅读:
with self.assertRaises(Exception):
do_something_that_throws("please")
Using these assertions you can test pretty much anything!
使用这些断言,您几乎可以测试任何东西!
模拟 (Mocking)
Recall that unit testing is testing individual parts of system in isolation. But, this is never the case with the systems we design. Different parts depend on other parts, and to make testing possible you sometimes need to mock them out. For example, if you are testing the behaviour of an HTTP response parser, there is no need to perform an actual HTTP response: all you need to do it mock it.
回想一下,单元测试在隔离测试系统的各个部分。 但是,我们设计的系统从来没有这种情况。 不同的部分取决于其他部分,为了使测试成为可能,您有时需要将它们模拟出来。 例如,如果您正在测试HTTP响应解析器的行为,则无需执行实际的HTTP响应:只需对其进行模拟即可。
Mocking, then, is a method of abstracting implementation of software modules that are not relevant to the behaviour you are testing. Moreover, you can assert if a mock was called, how many times was it called, and what arguments were supplied to it. These additional assertions will, no doubt, make your tests more robust.
那么,模拟是一种抽象化与所测试行为无关的软件模块实现的方法。 此外,您可以断言是否调用了模拟程序,调用了多少次以及向其提供了哪些参数。 毫无疑问,这些额外的断言将使您的测试更加强大。
Mocking in Python is also available via the unittest
module. Let's start with a simple example. Suppose you wrote a function that calls the supplied callback:
也可以通过unittest
模块使用Python进行模拟。 让我们从一个简单的例子开始。 假设您编写了一个调用提供的回调的函数:
def call_this_function(func):
func()
To test it, you just need to pass a MagicMock
(available through unittest.mock
):
要对其进行测试,您只需传递一个MagicMock
(可通过unittest.mock
):
import unittest
from unittest.mock import MagicMock
def call_this_function(func):
func()
class CallbackTestCase(unittest.TestCase):
def test_callback(self):
mock = MagicMock()
call_this_function(mock)
mock.assert_called_once()
if __name__ == '__main__':
unittest.main()
MagicMock
is a special type of object. You can call it itself, call any method you can come up with and it will never throw. Instead, it will memorize all calls and make them available to you through assertions. Note that in this case, assertions are called on the mock
object, instead of self
. Here are some of the assertions available for mock objects:
MagicMock
是一种特殊的对象。 您可以调用它本身,调用可以使用的任何方法,并且它永远不会抛出。 相反,它将记住所有调用,并通过断言使您可以使用它们。 请注意,在这种情况下,断言是在mock
对象上调用的,而不是self
。 以下是一些可用于模拟对象的断言:
assert_called
assert_called
assert_called_once
assert_called_once
assert_called_with
assert_called_with
assert_called_once_with
assert_called_once_with
assert_not_called
assert_not_called
模拟进口 (Mocking imports)
Sometimes, when you are testing a class, you need to mock out a certain function or class that is imported in it. Consider this example:
有时,当您测试一个类时,您需要模拟其中导入的某个函数或类。 考虑以下示例:
from some_library import api_action
def do_stuff():
result = api_action('/stuff')
return result
Now, to test this code, you want to mock the api_action
function. This is actually very easy to do with the patch
decorator (available from unittest.mock
):
现在,要测试此代码,您想模拟api_action
函数。 使用patch
装饰器实际上很容易做到(可从unittest.mock
):
import unittest
from unittest.mock import patch
from main import do_stuff
class ApiTestCase(unittest.TestCase):
@patch('main.api_action', side_effect='abacaba')
def test_stuff(self, api_action_mock):
result = do_stuff()
self.assertEqual(result, 'abacaba')
api_action_mock.assert_called_once_with('/stuff')
if __name__ == '__main__':
unittest.main()
The patch
decorator is applied to the test function that we are working with. It accepts as an argument the path to the mocked module. There is a very important note here: you specify the path relative to the testing code. For example, the main.py
file imported the api_action
function from some_library
. Then, we import the main
module into our test file. This means that we mock the api_action
function that is imported inside the main
, hence the 'main.api_action'
path. If we were to write 'some_library.api_action'
, this would not work. We also specify the side_effect
argument, which is essentially the return value that will be returned by the mock. The mock object is then passed as an argument to test function so we can assert on it. We assert that (1) the return value is forwarded and (2) that api_action
is called on the correct endpoint.
patch
装饰器将应用于我们正在使用的测试功能。 它接受模拟模块的路径作为参数。 这里有一个非常重要的说明:您指定相对于测试代码的路径。 例如, main.py
文件从some_library
导入了api_action
函数。 然后,我们将main
模块导入到我们的测试文件中。 这意味着我们模拟了在main
内部导入的api_action
函数,因此是'main.api_action'
路径。 如果我们要写'some_library.api_action'
,那将行不通。 我们还指定了side_effect
参数,该参数本质上是模拟将返回的返回值。 然后将模拟对象作为参数传递给测试函数,以便我们对其进行断言。 我们断言(1)转发了返回值,并且(2)在正确的端点上调用了api_action
。
结束语 (Closing notes)
Thank you for reading this article, I hope you liked it. Let me know in the comments about your experience with testing in Python!
感谢您阅读本文,希望您喜欢它。 在评论中让我知道您的Python测试经验!
python中的单元测试