ホームページ  >  記事  >  バックエンド開発  >  Python スキルを向上させる: 単体テストを理解する

Python スキルを向上させる: 単体テストを理解する

高洛峰
高洛峰オリジナル
2016-10-17 16:49:231113ブラウズ

新しいプログラム開発者にとって、最も一般的な混乱の 1 つはテストの主題です。彼らは漠然と「単体テスト」が良いと感じており、単体テストも行うべきだと感じています。しかし、彼らはその言葉の本当の意味を理解していません。そう思われる場合も、心配する必要はありません。この記事では、単体テストとは何か、それがなぜ役立つのか、Python でコードを単体テストする方法を紹介します。

テストとは何ですか?

なぜテストが役立つのか、またテストをどのように実施するのかを説明する前に、実際に「単体テスト」が何なのかを定義するために数分時間をとってみましょう。一般的なプログラミング用語では、「テスト」とは、プログラムにバグがあるかどうかを判断するために呼び出すことができるコード (実際のアプリケーションから独立したコード) を作成することを指します。これはコードが正しいことを証明するものではありません (非常に限られた場合にのみ可能性があります)。状況が正しく処理されたとテスターが信じているかどうかを報告するだけです。

注: 「テスト」を一度使用した場合は、「自動テスト」を指します。つまり、これらのテストはマシン上で実行されます。 「手動テスト」は、人がプログラムを実行し、それと対話して脆弱性を見つける別の概念です。

このテストではどのような状態を検出できますか?文法エラーとは、

my_list..append(foo)

の後の余分な「.」など、言語の偶発的な誤用です。論理エラーは、アルゴリズム (「問題を解決する方法」と考えることができます) が間違っている場合に発生します。おそらくプログラマは、Python が「ゼロインデックス」であることを忘れて、

print(my_string[len(my_string)])

と書いて文字列の最後の文字を出力しようとしたのかもしれません (IndexError が発生します)。より大きく、より体系的なエラーもチェックできます。たとえば、ユーザーが 100 を超える数値を入力するとプログラムがクラッシュし続けたり、Web サイトの検索が利用できない場合に Web サイトがハングしたりします。

これらのエラーはすべて、コードを注意深くテストすることでチェックできます。単体テストとは、特に、分離されたコード単位でのテストを指します。ユニットは、モジュール全体、単一のクラスまたは関数、またはその間のものにすることができます。ただし、テスト コードを、テストしていない他のコードから分離することが重要です (他のコード自体にエラーがあり、テスト結果が混乱するため)。次の例を考えてみましょう:

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 という 2 つの関数があります。 print_next_prime をテストする場合は、この関数が print_next_prime で呼び出されるため、is_prime が正しいことを確認する必要があります。この場合、print_next_prime 関数が 1 つの単位となり、is_prime 関数が別の単位になります。単体テストは一度に 1 つのユニットしかテストしないため、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 を通じて。 Unittest.main() が呼び出されると、test で始まる名前のメンバー関数が実行されます。これらは、unittest.TestCase の派生クラスであり、アサートされます。 python test_primes.py と入力してテストを実行すると、コンソールにunittest フレームワークの出力が表示されます。

$ 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

個々の「E」は単体テストの結果を表します (成功した場合は、 「。」が出力されます。テストが失敗したこと、失敗の原因となったコード行、スローされた例外情報も確認できます。

なぜテストするのですか?

その例を続ける前に、「なぜテストが私にとって価値があるのか​​?」と問うことが重要です。これはもっともな質問であり、コード テストを初めて行う人からよく尋ねられる質問です。結局のところ、テストにはある程度の時間がかかり、その時間を最も生産的なことを行う代わりにコードを書くために使用できるのです。

この質問に効果的に答えることができる答えはたくさんありますが、私は次の点をリストしました:

テストは、一連の与えられた条件の下でコードが適切に動作することを保証します

テストは、一連の条件の下で正確さを保証します。構文エラーは基本的にテストによって検出する必要があり、コード単位の基本ロジックも、特定の条件下での正しさを保証するテストによって検出できます。繰り返しますが、コードがすべての条件下で正しいことを証明することではありません。ここでは、考えられる条件の比較的完全なセットをターゲットにしているだけです (たとえば、my_addition_function(3, 'refrigerator) の呼び出しを検出するテストを作成できますが、必ずしも各引数の考えられるすべての文字を検出する必要はありません)。 string)

テストにより、コードへの変更によって既存の機能が損なわれないことを確認できます

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

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

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