>  기사  >  백엔드 개발  >  Python 기술 향상: 단위 테스트 이해

Python 기술 향상: 단위 테스트 이해

高洛峰
高洛峰원래의
2016-10-17 16:49:231113검색

신규 프로그램 개발자가 가장 흔히 혼동하는 것 중 하나가 테스트 대상입니다. 그들은 막연하게 "단위 테스트"가 좋다고 생각하고, 단위 테스트도 해야 한다고 생각합니다. 그러나 그들은 그 단어의 진정한 의미를 이해하지 못합니다. 만약 당신처럼 들린다면, 두려워하지 마세요! 이 기사에서는 단위 테스트가 무엇인지, 왜 유용한지, Python에서 코드를 단위 테스트하는 방법을 소개합니다.

테스트란 무엇인가요?

테스트가 왜 유용한지, 테스트를 수행하는 방법을 논의하기 전에 "단위 테스트"가 실제로 무엇인지 잠시 정의해 보겠습니다. 일반적인 프로그래밍 용어에서 "테스트"는 프로그램에 버그가 있는지 확인하는 데 도움이 되도록 호출할 수 있는 코드(실제 응용 프로그램과 독립적인 코드)를 작성하는 것을 의미합니다. 이는 코드가 정확하다는 것을 증명하지 않습니다(매우 제한된 경우에만 가능함). 이는 테스터가 상황이 올바르게 처리되었다고 믿는지 여부를 간단히 보고합니다.

참고: "테스트"를 한 번 사용하면 "자동 테스트"를 의미합니다. 즉, 이러한 테스트는 머신에서 실행됩니다. "수동 테스트"는 사람이 프로그램을 실행하고 상호 작용하여 취약점을 찾는 별도의 개념입니다.

테스트를 통해 어떤 상황을 감지할 수 있나요? 문법 오류는

my_list..append(foo)

뒤에 "."을 추가하는 등 실수로 인한 언어 오용입니다. 논리적 오류는 알고리즘("문제 해결 방법"이라고 생각할 수 있음)이 올바르지 않을 때 발생합니다. 어쩌면 프로그래머는 Python이 "제로 인덱스"라는 사실을 잊고 마지막에

print(my_string[len(my_string)])

(IndexError가 발생함)을 작성하여 문자열을 인쇄하려고 시도했을 수도 있습니다. 성격. 더 크고 더 체계적인 오류도 확인할 수 있습니다. 예를 들어, 사용자가 100보다 큰 숫자를 입력하면 프로그램이 계속 충돌하거나 웹 사이트 검색을 사용할 수 없을 때 웹 사이트가 중단됩니다.

이러한 모든 오류는 코드를 주의 깊게 테스트하여 확인할 수 있습니다. 단위 테스트는 특히 별도의 코드 단위로 테스트하는 것을 의미합니다. 단위는 전체 모듈, 단일 클래스나 함수 또는 그 사이의 모든 것일 수 있습니다. 그러나 테스트 코드는 우리가 테스트하지 않는 다른 코드와 격리되는 것이 중요합니다(다른 코드 자체에 오류가 있어 테스트 결과를 혼동시키기 때문입니다). 다음 예를 고려하십시오.

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)

is_prime 및 print_next_prime이라는 두 가지 함수가 있습니다. print_next_prime을 테스트하려면 is_prime이 올바른지 확인해야 합니다. 이 함수는 print_next_prime에서 호출되기 때문입니다. 이 경우 print_next_prime 함수는 하나의 단위이고 is_prime 함수는 또 다른 단위입니다. 단위 테스트는 한 번에 하나의 단위만 테스트하므로 print_next_prime?을 정확하게 테스트하는 방법에 대해 신중하게 생각해야 합니다(이러한 테스트를 구현하는 방법은 나중에 자세히 설명).

그럼 테스트 코드는 어떤 모습이어야 할까요? 이전 예제가 primes.py라는 파일에 있으면 test_primes.py라는 파일에 테스트 코드를 작성할 수 있습니다. 다음은 테스트 샘플과 같은 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()

이 파일은 테스트 사례를 통해 생성됩니다. test_is_five_prime.단위 테스트입니다. 단위 테스트를 통해 Python에 내장된 테스트 프레임워크입니다. Unittest.main()이 호출되면 test로 시작하는 이름의 모든 멤버 함수가 실행됩니다. 이 함수는 unittest.TestCase의 파생 클래스이며 어설션됩니다. 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

”는 단위 테스트의 결과를 나타냅니다(성공하면 “. "가 인쇄됩니다.) 실패를 일으킨 코드 줄 및 발생한 예외 정보와 함께 테스트가 실패했음을 확인할 수 있습니다.

왜 테스트하나요?

이 예를 계속하기 전에 "테스트가 나에게 왜 가치가 있습니까?"라고 질문하는 것이 중요합니다. 이는 공정한 질문이며, 코드 테스트를 처음 접하는 사람들이 자주 묻는 질문입니다. 결국 테스트에는 일정 시간이 걸리며 그 시간을 사용하여 가장 생산적인 작업을 수행하는 대신 테스트할 이유가 무엇입니까?

이 질문에 효과적으로 답할 수 있는 답변이 많이 있으며 다음 사항을 나열했습니다.

테스트를 통해 주어진 조건에서 코드가 제대로 작동하는지 확인할 수 있습니다.

테스트는 다양한 조건에서 정확성을 보장합니다. 구문 오류는 기본적으로 테스트를 통해 감지해야 하며, 특정 조건에서 정확성을 보장하기 위해 코드 단위의 기본 논리도 테스트를 통해 감지할 수 있습니다. 다시 말하지만, 모든 조건에서 코드가 정확하다는 것을 증명하는 것은 아닙니다. 우리는 단순히 가능한 조건의 상대적으로 완전한 세트를 목표로 하고 있습니다(예를 들어 my_addition_function(3, 'refrigerator)를 호출할 때를 감지하는 테스트를 작성할 수 있지만 반드시 각 인수에 대해 가능한 모든 문자를 감지할 필요는 없습니다) 문자열)

테스트를 통해 코드 변경으로 인해 기존 기능이 중단되지 않는지 확인할 수 있습니다.

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

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

编写测试强迫你去思考在非正常条件下你的代码可能遇到的问题。在上面的例子中,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/


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.