単体テストを議論するとき、テストで完全なコードカバレッジを行う必要があるかどうかは、かなり頻繁で議論のあるトピックです。 ほとんどの開発者は、それを行う必要はなく、非効率的で役に立たないと考える傾向がありますが、私は反対意見を持っています(少なくともPythonで開発する場合)。 この記事では、コードを完全にカバーする方法の例を示し、開発経験に基づいた完全なカバーの欠点と利点を説明します。
鼻テストツール
単体テストと統計の収集には、
noseを使用し
ます 。 他の手段と比較した場合の利点:
- 単体テストをバインドするための追加コードを記述する必要はありません
- 特にカバレッジパーセンテージを計算するための組み込みメトリックツール
- Python 3互換( Googleコードの py3kブランチ)
noseをインストールしても問題は発生しません。easy_installを介してインストールされるか、ほとんどのLinuxリポジトリにインストールされるか、ソースから簡単にインストールできます。 Python 3の場合、py3kブランチのクローンを作成し、ソースからインストールする必要があります。
初期コード例
階乗計算がテストされます:
#!/usr/bin/env python
import operator
def factorial (n):
if n < 0 :
raise ValueError ( "Factorial can't be calculated for negative numbers." )
if type (n) is float or type (n) is complex :
raise TypeError ( "Factorial doesn't use Gamma function." )
if n == 0 :
return 1
return reduce (operator . mul, range ( 1 , n + 1 ))
if __name__ == '__main__' :
n = input ( 'Enter the positive number: ' )
print '{0}! = {1}' . format(n, factorial( int (n)))
コードはPython 2.6でのみ機能し、Python 3との互換性はありません。コードは
main.pyファイルに保存されます。
単体テスト
簡単なテストから始めましょう:
import unittest
from main import factorial
class TestFactorial (unittest . TestCase):
def test_calculation ( self ):
self . assertEqual( 720 , factorial( 6 ))
def test_negative ( self ):
self . assertRaises( ValueError , factorial, -1 )
def test_float ( self ):
self . assertRaises( TypeError , factorial, 1.25 )
def test_zero ( self ):
self . assertEqual( 1 , factorial( 0 ))
これらのテストは機能のみをテストします。 コードカバレッジ-83%:
$ nosetests --with-coverage --cover-erase
....
Name Stmts Exec Cover Missing
-------------------------------------
main 12 10 83% 16-17
----------------------------------------------------------------------
Ran 4 tests in 0.021s
OK
100%のカバレッジのために別のクラスを追加します。
class TestMain (unittest . TestCase):
class FakeStream :
def __init__ ( self ):
self . msgs = []
def write ( self , msg):
self . msgs . append(msg)
def readline ( self ):
return '5'
def test_use_case ( self ):
fake_stream = self . FakeStream()
try :
sys . stdin = sys . stdout = fake_stream
execfile ( 'main.py' , { '__name__' : '__main__' })
self . assertEqual( '5! = 120' , fake_stream . msgs[ 1 ])
finally :
sys . stdin = sys . __stdin__
sys . stdout = sys . __stdout__
これで、コードはテストで完全にカバーされます。
$ nosetests --with-coverage --cover-erase
.....
Name Stmts Exec Cover Missing
-------------------------------------
main 12 12 100%
----------------------------------------------------------------------
Ran 5 tests in 0.032s
OK
結論
これで、実際のコードに基づいて、いくつかの結論を導き出すことができます。
- 何よりもまず 、コードを完全に網羅していても、プログラムの機能を完全にチェックすることはできず、そのパフォーマンスを保証するものではありません。 この例では、完全なカバレッジが提供されましたが、引数の複雑なタイプを検証するテストはありませんでした。
- 少なくともPythonでは、コードを完全にカバーできます。 はい、組み込み関数で操作し、特定のメカニズムがどのように機能するかを知る必要がありますが、これは現実的であり、Python 3ではさらに簡単になりました。
- Pythonは動的に型指定されたプログラミング言語であり、ユニットテストは型チェックを行うのに役立ちます。 完全にカバーされているため、プログラム全体でタイピングが正しく実行される可能性がはるかに高くなります。
- 完全なカバレッジは、使用するライブラリのAPIを変更するとき、およびプログラミング言語自体を変更するときに役立ちます(以下のPython 3の例を参照)。 なぜなら すべてのコード行が呼び出されることが保証され、コードとAPIのすべての不整合が検出されます。
- 前の段落の結果として、完全なカバレッジはコードのテストに役立ちます。 たとえば、本番システムで作業する場合、ソフトウェア統合の前に、最初にテストできます。 多くの場合、通常のデバッグは不可能です(リモートシステムに権限がなく、管理者がすべてに従事している場合など)。ユニットテストは問題の場所を理解するのに役立ちます。
Python 3への適応
Python 3の適応を使用して、コードの完全なカバレッジが作業にどのように役立つかを示したいと思います。 したがって、最初にPython 3でプログラムを実行すると、構文エラーがスローされます。
$ python3 main.py
File "main.py", line 17
print '{0}! = {1}'.format(n, factorial(int(n)))
^
SyntaxError: invalid syntax
修正します:
#!/usr/bin/env python
import operator
def factorial (n):
if n < 0 :
raise ValueError ( "Factorial can't be calculated for negative numbers." )
if type (n) is float or type (n) is complex :
raise TypeError ( "Factorial doesn't use Gamma function." )
if n == 0 :
return 1
return reduce (operator . mul, range ( 1 , n + 1 ))
if __name__ == '__main__' :
n = input ( 'Enter the positive number: ' )
print ( '{0}! = {1}' . format(n, factorial( int (n))))
これで、プログラムを起動できます。
$ python3 main.py
Enter the positive number: 0
0! = 1
これは、プログラムが機能しているということですか?
いや! reduceが呼び出されるまでのみ機能します。テストから次のことがわかります。
$ nosetests3
E...E
======================================================================
ERROR: test_calculation (tests.TestFactorial)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/nuald/workspace/factorial/tests.py", line 9, in test_calculation
self.assertEqual(720, factorial(6))
File "/home/nuald/workspace/factorial/main.py", line 12, in factorial
return reduce(operator.mul, range(1, n + 1))
NameError: global name 'reduce' is not defined
======================================================================
ERROR: test_use_case (tests.TestMain)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/nuald/workspace/factorial/tests.py", line 38, in test_use_case
execfile('main.py', {'__name__': '__main__'})
NameError: global name 'execfile' is not defined
----------------------------------------------------------------------
Ran 5 tests in 0.010s
FAILED (errors=2)
この例では、これはすべて手動テストで検出できます。 ただし、大規模なプロジェクトでは、この種のエラーの検出に役立つのは単体テストのみです。 また、コードを完全に網羅することでのみ、コードとAPIのほぼすべての不整合が解決されたことを保証できます。
実は、実際のコードはPython 2.6とPython 3の間で完全に互換性があります:
#!/usr/bin/env python
import operator
from functools import reduce
def factorial (n):
if n < 0 :
raise ValueError ( "Factorial can't be calculated for negative numbers." )
if type (n) is float or type (n) is complex :
raise TypeError ( "Factorial doesn't use Gamma function." )
if n == 0 :
return 1
return reduce (operator . mul, range ( 1 , n + 1 ))
if __name__ == '__main__' :
n = input ( 'Enter the positive number: ' )
print ( '{0}! = {1}' . format(n, factorial( int (n))))
import sys
import unittest
from main import factorial
class TestFactorial (unittest . TestCase):
def test_calculation ( self ):
self . assertEqual( 720 , factorial( 6 ))
def test_negative ( self ):
self . assertRaises( ValueError , factorial, -1 )
def test_float ( self ):
self . assertRaises( TypeError , factorial, 1.25 )
def test_zero ( self ):
self . assertEqual( 1 , factorial( 0 ))
class TestMain (unittest . TestCase):
class FakeStream :
def __init__ ( self ):
self . msgs = []
def write ( self , msg):
self . msgs . append(msg)
def readline ( self ):
return '5'
def test_use_case ( self ):
fake_stream = self . FakeStream()
try :
sys . stdin = sys . stdout = fake_stream
obj_code = compile ( open ( 'main.py' ) . read(), 'main.py' , 'exec' )
exec (obj_code, { '__name__' : '__main__' })
self . assertEqual( '5! = 120' , fake_stream . msgs[ 1 ])
finally :
sys . stdin = sys . __stdin__
sys . stdout = sys . __stdout__
テストでは、さまざまなバージョンのPythonでのプログラムの完全なカバレッジとパフォーマンスが示されます。
$ nosetests --with-coverage --cover-erase
.....
Name Stmts Exec Cover Missing
-------------------------------------
main 13 13 100%
----------------------------------------------------------------------
Ran 5 tests in 0.038s
OK
$ nosetests3 --with-coverage --cover-erase
.....
Name Stmts Miss Cover Missing
-------------------------------------
main 13 0 100%
----------------------------------------------------------------------
Ran 5 tests in 0.018s
OK
おわりに
完全なコードカバレッジは、プログラムエラーから保護できる万能薬ではありません。 ただし、これは知って使用する必要があるツールです。 完全なカバレッジには多くの利点がありますが、本質的に1つの欠点しかありません。テストを書くのに必要な時間とリソースです。 ただし、テストを記述するほど、将来的にテストが簡単になります。 私たちのプロジェクトでは、1年以上にわたってコードを100%網羅しています。最初は多くの問題がありましたが、コードを完全に網羅することはまったく問題ではありません。 すべてのメソッドが完成し、すべての必要なパッケージが作成されました。 ここには魔法はありませんが(Pythonの魔法を使用する必要があります)、開始するだけです。
PS Fullカバレッジにはもう1つの利点があります。これは完全に明確ではありませんが、自分がプロであると考える人にとっては間違いなく重要です。 この種の知識は、すべての人、特にライブラリ開発者に役立ちます。