Home  >  Article  >  Backend Development  >  Improve your Python skills: Understanding unit testing

Improve your Python skills: Understanding unit testing

高洛峰
高洛峰Original
2016-10-17 16:49:231113browse

For new program developers, one of the most common confusions is the subject of testing. They vaguely feel that "unit testing" is good, and they should do unit testing too. But they don't understand the true meaning of the word. If this sounds like you, have no fear! In this article, I will introduce what unit testing is, why it is useful, and how to unit test your code in Python.

What is a test?

Before discussing why testing is useful and how to conduct testing, let’s take a few minutes to define what “unit testing” actually is. In general programming terms, "testing" refers to writing code that you can call (code that is independent of your actual application) to help you determine whether there are bugs in your program. This doesn't prove that your code is correct (it's the only possibility in very limited cases). It simply reports whether the tester believes that the situation was handled correctly.

Note: When I use "test" once, I am referring to "automated tests", i.e. these tests are run on the machine. "Manual testing" is a separate concept in which a person runs a program and interacts with it to find vulnerabilities.

What kind of conditions can the test detect? Grammatical errors are accidental misuses of language, such as an extra "." after

my_list..append(foo)

. Logical errors are caused when an algorithm (which can be thought of as a "way of solving a problem") is incorrect. Maybe the programmer forgot that Python is "zero-indexed" and tried to print out the last character in a string by writing

print(my_string[len(my_string)])

(which would raise an IndexError). Larger, more systematic errors can also be checked. For example, the program will keep crashing when the user enters a number greater than 100 or hangs the website when the website search is unavailable.

All these errors can be checked by careful testing of the code. Unit testing refers specifically to testing in a separated unit of code. A unit can be an entire module, a single class or function, or anything in between. However, it is important that the test code is isolated from other code that we are not testing (because other code itself has errors and thus confuses the test results). Consider the following example:

def is_prime(number):
    """Return True if *number* is prime."""
    for element in range(number):
        if number % element == 0:
            return False
    return True
def print_next_prime(number):
    """Print the closest prime number larger than *number*."""
    index = number
    while True:
        index += 1
        if is_prime(index):
            print(index)

You have two functions, is_prime and print_next_prime. If you want to test print_next_prime, we need to make sure is_prime is correct because this function is called in print_next_prime. In this case, the print_next_prime function is one unit and the is_prime function is another unit. Since unit tests only test one unit at a time, we need to think carefully about how to accurately test print_next_prime? (more on how to implement these tests later).

So, what should the test code look like? If the previous example exists in a file called primes.py, we can write the test code in a file called test_primes.py. The following is the most basic content in test_primes.py, such as the following test sample:

import unittest
from primes import is_prime
class PrimesTestCase(unittest.TestCase):
    """Tests for `primes.py`."""
    def test_is_five_prime(self):
        """Is five successfully determined to be prime?"""
        self.assertTrue(is_prime(5))
if __name__ == '__main__':
    unittest.main()

This file creates a unit test through a test case:?test_is_five_prime. Through unittest, a testing framework built into Python. When unittest.main() is called, any member function named starting with test will be run. They are a derived class of unittest.TestCase and are asserted. If we run the test by typing python test_primes.py, we can see the output of the unittest framework on the console:

$ python test_primes.py
E
======================================================================
ERROR: test_is_five_prime (__main__.PrimesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 8, in test_is_five_prime
    self.assertTrue(is_prime(5))
File "/home/jknupp/code/github_code/blug_private/primes.py", line 4, in is_prime
    if number % element == 0:
ZeroDivisionError: integer division or modulo by zero
----------------------------------------------------------------------
Ran 1 test in 0.000s

The individual "E" represents the result of our unit test (if it succeeded , a "." will be printed out). We can see that our test failed, along with the line of code that caused the failure, and any exception information that was thrown.

Why test?

Before we continue with that example, it’s important to ask: “Why is testing valuable to me”? It's a fair question, and one that's often asked by people who are new to code testing. After all, testing takes a certain amount of time, and we can use that time to write code. Why test instead of doing the most productive things?

There are many answers that can effectively answer this question, I have listed the following points:

Testing ensures that your code works properly under a set of given conditions

Testing ensures correctness under a set of conditions. Syntax errors must basically be detected through testing, and the basic logic of a code unit can also be detected through testing to ensure correctness under certain conditions. Again, it's not about proving that the code is correct under all conditions. We're simply targeting a relatively complete set of possible conditions (for example, you could write a test that detects when you call my_addition_function(3, 'refrigerator), but you don't necessarily need to detect every possible character for each argument) string)

Testing allows people to ensure that changes to code do not break existing functionality

重构代码时,这一点特别有用。如果没有测试到位,你就没法保证你的代码的改变没有破坏之前工作正常的东西。如果你希望更改或重写你的代码,并希望不会破坏任何东西,适当的单元测试是很必要的。

测试迫使人们在不寻常条件的情况下思考代码,这可能会揭示出逻辑错误

编写测试强迫你去思考在非正常条件下你的代码可能遇到的问题。在上面的例子中,my_addition_function函数可以将两个数字相加。测试基本正确性的简单测试将调用my_addition_function(2,2),并断言说结果是4。然而,进一步的测试可能会通过调用my_addition_function(2.0,2.0)来测试该功能是否能正确进行浮点数的运算。防御性的编码原则表明你的代码应该能够在非法输入的情况下正常失效,因此测试时,当字符串类型被作为参数传递到函数中时应当抛出一个异常。

良好的测试要求模块化,解耦代码,这是一个良好的系统设计的标志

单元测试的整体做法是通过代码的松散耦合使其变得更容易。如果你的应用程序代码直接调用数据库,例如,测试你应用程序的逻辑依赖于一个有效的数据库连接,并且测试数据要存在于数据库中。另一方面,隔离了外部资源的代码在测试过程中更容易被模拟对象所替代。出于必要,(人们)设计的有测试能力的应用程序最终采用了模块化和松散耦合。

单元测试的剖析

通过继续之前的例子,我们将看到如何编写并组织单元测试。回想一下,primes.py包含以下代码:

def is_prime(number):
    """Return True if *number* is prime."""
    for element in range(number):
        if number % element == 0:
            return False
    return True
def print_next_prime(number):
    """Print the closest prime number larger than *number*."""
    index = number
    while True:
        index += 1
        if is_prime(index):
            print(index)

   

同时,文件test_primes.py包含如下代码:

import unittest
from primes import is_prime
class PrimesTestCase(unittest.TestCase):
    """Tests for `primes.py`."""
    def test_is_five_prime(self):
        """Is five successfully determined to be prime?"""
        self.assertTrue(is_prime(5))
if __name__ == '__main__':
    unittest.main()

   

做出断言

unittest是Python标准库中的一部分,并且也是我们开始“单元测试之旅”的一个好的起点。一个单元测试中包括一个或多个断言(一些声明被测试代码的一些属性为真的语句)。会想你上学的时候“断言”这个词的字面意思就是“陈述事实”。在单元测试中,断言也是同样的作用。

self.assertTrue 更像是自我解释。它能声明传递过去的参数的计算结果为真。unittest.TestCase类包含了许多断言方法,所以一定要检查列表并选择合适的方法进行测试。如果在每个测试中都用到assertTrue的话,则应该考虑一个反模式,因为它增加了测试中读者的认知负担。正确使用断言的方法应当是使测试能够明确说明究竟是什么在被断言(例如,很明显?,只需扫一眼assertIsInstance 的方法名,就知道它要说明的是其参数)。

每个测试应该测试一个单独、有具体特性的代码,并且应该被赋予相关的命名。就单元测试发现机制的研究表明(主要在Python2.7+和3.2+版本中),测试方法应该以test_为前缀命名。(这是可配置的,但是其目的是鉴别测试方法和非测试的实用方法)。如果我们把test_is_five_prime 的命名改为is_five_prime的话,运行python中的test_primes.py时会输出如下信息:

$ python test_primes.py
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK

   

不要被上面信息中的“OK”所糊弄了,只有当什么测试都没真正运行的时候才会显示出“OK”!我认为一个测试也没跑其实应该显示个报错的,但是个人感觉放在一边,这是一个你应该注意是行为,尤其是当通过程序运行来检查测试结果的时候(例如,一个持续的集成工具,像TracisCI)。

异常

让我们回到test_primes.py的实际内容中去,回忆一下运行python test_primes.py指令后的输出结果:

$ python test_primes.py
E
======================================================================
ERROR: test_is_five_prime (__main__.PrimesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 8, in test_is_five_prime
    self.assertTrue(is_prime(5))
File "/home/jknupp/code/github_code/blug_private/primes.py", line 4, in is_prime
    if number % element == 0:
ZeroDivisionError: integer division or modulo by zero
----------------------------------------------------------------------
Ran 1 test in 0.000s

   

这些输出告诉我们,我们一个测试的结果失败并不是因为一个断言失败了,而是因为出现了一个未捕获的异常。事实上,由于抛出了一个异常,unittest框架并没有能够运行我们的测试就返回了。

这里的问题很明确:我们使用的求模运算的计算范围中包括了0,因此执行了一个除以0的操作。为了解决这个问题,我们可以很简单的将起始值由0变为2,并指出对0求模是错误的,而对1求模则一直是真(并且一个素数只能被自身和1整除,因此我们无需检查1)。

解决问题

一次失败的测试使我们修改了代码。一旦我们改好了这个错误(将s_prime中的一行改为for element in range(2, number):),我们就得到了如下输出:

$ python test_primes.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

   

现在错误已经改了,这是不是意味着我们应该删掉test_is_five_prime这个测试方法(因为很明显,它将不会一直能通过测试)?不应该删。由于通过测试是最终目标的话单元测试应该尽量少的被删除。我们已经测试过is_prime的语法是有效的,并且,至少在一种情况下,它返回正确的结果。我们的目标是要建立一套能全部通过的(单元测试的逻辑分组)测试,虽然有些一开始可能会失败。

test_is_five_prime用于处理一个“非特殊”的素数。让我们确保它也能正确处理非素数。将以下方法添加到PrimesTestCase类:

def test_is_four_non_prime(self):
    """Is four correctly determined not to be prime?"""
    self.assertFalse(is_prime(4), msg='Four is not prime!')

   

请注意,这时我们给assert调用添加了可选的msg参数。如果该测试失败了,我们的信息将被打印到控制台,并给运行测试的人提供额外的信息。

边界情况

我们已经成功的测试了两种普通情况。现在让我们考虑边界情况下、或者那些不寻常或意外的输入的用例。当测试一个其范围是正整数的函数时,边界情况下的实例包括0、1、负数和一个很大的数字。现在让我们来测试其中的一些。

添加一个对0的测试很简单。我们预计?is_prime(0)返回的是false,因为,根据定义,素数必须大于1。

def test_is_zero_not_prime(self):
    """Is zero correctly determined not to be prime?"""
    self.assertFalse(is_prime(0))

   

可惜呀,输出是:

python test_primes.py
..F
======================================================================
FAIL: test_is_zero_not_prime (__main__.PrimesTestCase)
Is zero correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 17, in test_is_zero_not_prime
    self.assertFalse(is_prime(0))
AssertionError: True is not false
----------------------------------------------------------------------
Ran 3 tests in 0.000s
FAILED (failures=1)

   

0被错误的判定为素数。我们忘记了,我们决定在数字范围中跳过0和1。让我们增加一个对他们的特殊检查。

def is_prime(number):
    """Return True if *number* is prime."""
    if number in (0, 1):
        return False
    for element in range(2, number):
        if number % element == 0:
            return False
    return True

   

现在测试通过了。我们的函数应该怎样处理一个负数?在写这个测试用例之前就知道输出结果是很重要的。在这种情况下,任何负数都应该返回false。

def test_negative_number(self):
    """Is a negative number correctly determined not to be prime?"""
    for index in range(-1, -10, -1):
        self.assertFalse(is_prime(index))

   

这里我们觉得检查从-1到-9的所有数字。在一个循环中调用test方法是非常合法的,在一个测试中多次调用断言方法也可以。我们可以在下面用(更详细)的方式改写代码。

def test_negative_number(self):
    """Is a negative number correctly determined not to be prime?"""
    self.assertFalse(is_prime(-1))
    self.assertFalse(is_prime(-2))
    self.assertFalse(is_prime(-3))
    self.assertFalse(is_prime(-4))
    self.assertFalse(is_prime(-5))
    self.assertFalse(is_prime(-6))
    self.assertFalse(is_prime(-7))
    self.assertFalse(is_prime(-8))
    self.assertFalse(is_prime(-9))

   

这两个是完全等价的。除了当我们运行循环版本时,我们得到了一个我们不太想要的信息:

python test_primes.py
...F
======================================================================
FAIL: test_negative_number (__main__.PrimesTestCase)
Is a negative number correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_primes.py", line 22, in test_negative_number
    self.assertFalse(is_prime(index))
AssertionError: True is not false
----------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (failures=1)

   

嗯···我们知道测试失败了,但是是在哪个负数上失败的?非常没用的是,Python的单元测试框架并没有打印出预期值和实际值。我们可以移步到两种方式上,并用其中之一来解决问题:通过msg参数,或通过使用一个第三方的单元测试框架。

使用msg参数来assertFalse仅仅能够使我们认识到我们可以用字符串的格式设置来解决问题。

def test_negative_number(self):
    """Is a negative number correctly determined not to be prime?"""
    for index in range(-1, -10, -1):
        self.assertFalse(is_prime(index), msg='{} should not be determined to be prime'.format(index))

   

从而给出了如下输出信息:

python test_primes
...F
======================================================================
FAIL: test_negative_number (test_primes.PrimesTestCase)
Is a negative number correctly determined not to be prime?
----------------------------------------------------------------------
Traceback (most recent call last):
File "./test_primes.py", line 22, in test_negative_number
    self.assertFalse(is_prime(index), msg='{} should not be determined to be prime'.format(index))
AssertionError: True is not false : -1 should not be determined to be prime
----------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (failures=1)

   

妥善地修复代码

我们看到,失败的负数是第一个数字:-1。为了解决这个问题,我们可以为负数增再增加一个特殊检查,但是编写单元测试的目的不是盲目的添加代码来检测边界情况。当一个测试失败时,我们应该退后一步并且确定解决问题的最佳方式。在这种情况下,我们就不该增加一个额外的if:

def is_prime(number):
    """Return True if *number* is prime."""
    if number < 0:
        return False
    if number in (0, 1):
        return False
    for element in range(2, number):
        if number % element == 0:
            return False
    return True

   

应当首先使用如下代码:

def is_prime(number):
    """Return True if *number* is prime."""
    if number <= 1:
        return False
    for element in range(2, number):
        if number % element == 0:
            return False
    return True

   

在后一个代码中,我们发现如果参数小于等于1时,两个if语句可以合并到一个返回值为false的语句中。这样做不仅更加简洁,并且很好的贴合了素数的定义(一个比1大并且只能被1和它本身整除的数)。

第三方测试框架

我们本来也可以通过使用第三方测试框架解决这个由于信息太少导致测试失败的问题。最常用的两个是py.test和nose。通过运行语句py.test -l(-l为显示局部变量的值)可以得到如下结果。

#! bash
py.test -l test_primes.py
============================= test session starts ==============================
platform linux2 -- Python 2.7.6 -- pytest-2.4.2
collected 4 items
test_primes.py ...F
=================================== FAILURES ===================================
_____________________ PrimesTestCase.test_negative_number ______________________
self =     def test_negative_number(self):
        """Is a negative number correctly determined not to be prime?"""
        for index in range(-1, -10, -1):
>           self.assertFalse(is_prime(index))
E           AssertionError: True is not false
index      = -1
self       = test_primes.py:22: AssertionError


正如你所看到的,一些更有用的信息。这些框架提供了比单纯的更详细的输出更多的功能,但问题是仅仅知道它们能存在和扩展内置unittest测试包的功能。

结束语

在这篇文章中,你学到了什么是单元测试,为什么它们如此重要,还有怎样编写测试。这就是说,要注意我们只是剖开了测试方法学中的表层,更多高级的话题,比如测试案例的组织、持续整合以及测试案例的管理等都是可供那些想要进一步学习Python中的测试的读者研究的很好的话题。

在不改变其功能的前提下重组/清理代码

编代码时不暴露其内部数据或函数并且不使用其他代码的内部数据或函数


文章转自:http://blog.jobbole.com/55180/ 作者:卷卷怪

英文出处:http://jeffknupp.com/blog/2013/12/09/improve-your-python-understanding-unit-testing/


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn