Python Language
ユニットテスト
サーチ…
備考
Pythonにはいくつかのユニットテストツールがあります。このドキュメントのトピックでは、基本的なunittest
モジュールについて説明します。他のテストツールには、 py.test
とnosetests
ます。 テストに関するこのPythonのドキュメントは、これらのツールのいくつかを深く掘り下げずに比較します。
例外のテスト
例えば、誤った入力が与えられたときにプログラムがエラーを投げる。このため、実際に間違った入力があった場合にエラーがスローされるようにする必要があります。そのため、正確な例外をチェックする必要があります。この例では、次の例外を使用します。
class WrongInputException(Exception):
pass
この例外は、間違った入力が与えられた場合に発生します。次のコンテキストでは、数字がテキスト入力として常に期待されます。
def convert2number(random_input):
try:
my_input = int(random_input)
except ValueError:
raise WrongInputException("Expected an integer!")
return my_input
例外が発生したかどうかを確認するために、 assertRaises
を使用してその例外をチェックします。 assertRaises
は、次の2つの方法で使用できます。
- 通常の関数呼び出しを使用します。最初の引数は例外型をとり、2番目は呼び出し可能(通常は関数)で、残りの引数はこの呼び出し可能関数に渡されます。
-
with
節を使用しwith
、例外タイプのみを関数に与えます。これには、より多くのコードを実行できるという利点がありますが、複数の関数が同じ例外を使用する可能性があるため、問題になる可能性があるため注意して使用する必要があります。例:self.assertRaises(WrongInputException):convert2number( "not a number")
これは、次のテストケースで実装されています。
import unittest
class ExceptionTestCase(unittest.TestCase):
def test_wrong_input_string(self):
self.assertRaises(WrongInputException, convert2number, "not a number")
def test_correct_input(self):
try:
result = convert2number("56")
self.assertIsInstance(result, int)
except WrongInputException:
self.fail()
スローされてはならない例外をチェックする必要があるかもしれません。ただし、例外がスローされたときにテストが自動的に失敗するため、テストはまったく必要ない場合があります。オプションを表示するだけで、2番目のテストメソッドは、スローされない例外がどのようにチェックされるかについてのケースを示しています。基本的には、これは例外をキャッチし、 fail
メソッドを使用してテストにfail
ます。
unittest.mock.create_autospecを使った関数のモッキング
関数を模擬する1つの方法は、オブジェクトの仕様に従ってオブジェクトをモックアウトするcreate_autospec
関数を使うことcreate_autospec
。関数を使用することで、適切に呼び出されるようにすることができます。
custom_math.py
関数をmultiply
と:
def multiply(a, b):
return a * b
また、 process_math.py
関数multiples_of
:
from custom_math import multiply
def multiples_of(integer, *args, num_multiples=0, **kwargs):
"""
:rtype: list
"""
multiples = []
for x in range(1, num_multiples + 1):
"""
Passing in args and kwargs here will only raise TypeError if values were
passed to multiples_of function, otherwise they are ignored. This way we can
test that multiples_of is used correctly. This is here for an illustration
of how create_autospec works. Not recommended for production code.
"""
multiple = multiply(integer,x, *args, **kwargs)
multiples.append(multiple)
return multiples
私たちは、 multiply
嘲笑しmultiply
単独でmultiples_of
テストを行うことができます。以下の例では、Pythonの標準ライブラリunittestを使用していますが、これはpytestやnoseのような他のテストフレームワークでも使用できます。
from unittest.mock import create_autospec
import unittest
# we import the entire module so we can mock out multiply
import custom_math
custom_math.multiply = create_autospec(custom_math.multiply)
from process_math import multiples_of
class TestCustomMath(unittest.TestCase):
def test_multiples_of(self):
multiples = multiples_of(3, num_multiples=1)
custom_math.multiply.assert_called_with(3, 1)
def test_multiples_of_with_bad_inputs(self):
with self.assertRaises(TypeError) as e:
multiples_of(1, "extra arg", num_multiples=1) # this should raise a TypeError
unittest.TestCase内でのセットアップのテストとティアダウン
時々、各テストが実行されるためのコンテキストを準備する必要があります。 setUp
メソッドは、クラス内の各テストの前に実行されます。 tearDown
はすべてのテストの最後に実行されます。これらのメソッドはオプションです。 TestCasesは協調的な多重継承でよく使われるので、基本クラスのsetUp
メソッドとtearDown
メソッドも呼び出されるように、常にこれらのメソッドでsuper
を呼び出すように注意する必要があります。 TestCase
の基本実装では、空のsetUp
メソッドとtearDown
メソッドが用意されているため、例外を送出せずに呼び出すことができます。
import unittest
class SomeTest(unittest.TestCase):
def setUp(self):
super(SomeTest, self).setUp()
self.mock_data = [1,2,3,4,5]
def test(self):
self.assertEqual(len(self.mock_data), 5)
def tearDown(self):
super(SomeTest, self).tearDown()
self.mock_data = []
if __name__ == '__main__':
unittest.main()
addCleanup
+には、テストの実行後に呼び出される関数を登録するaddCleanup
メソッドもあります。 setUp
が成功した場合にのみ呼び出されるtearDown
とは対照的に、 setUp
未処理の例外が発生した場合でもaddCleanup
を介して登録された関数が呼び出されます。具体的な例として、このメソッドは、テストの実行中に登録されたさまざまなモックを削除することでよく見られます。
import unittest
import some_module
class SomeOtherTest(unittest.TestCase):
def setUp(self):
super(SomeOtherTest, self).setUp()
# Replace `some_module.method` with a `mock.Mock`
my_patch = mock.patch.object(some_module, 'method')
my_patch.start()
# When the test finishes running, put the original method back.
self.addCleanup(my_patch.stop)
この方法でクリーンアップを登録するもう1つの利点は、プログラマーがセットアップコードの隣にクリーンアップコードを置くことができ、サブクラスがtearDown
super
を呼び出すことを忘れた場合にあなたを保護することです。
例外に対するアサーション
関数が2つの異なるメソッドを介して組込みunittestで例外をスローすることをテストできます。
コンテキストマネージャを使用する
def division_function(dividend, divisor):
return dividend / divisor
class MyTestCase(unittest.TestCase):
def test_using_context_manager(self):
with self.assertRaises(ZeroDivisionError):
x = division_function(1, 0)
これによりコンテキストマネージャの内部でコードが実行され、成功した場合は例外が発生しなかったためテストに失敗します。コードが正しい型の例外を発生させた場合、テストは続行されます。
追加のアサーションを実行する場合は、発生した例外の内容を取得することもできます。
class MyTestCase(unittest.TestCase):
def test_using_context_manager(self):
with self.assertRaises(ZeroDivisionError) as ex:
x = division_function(1, 0)
self.assertEqual(ex.message, 'integer division or modulo by zero')
呼び出し可能な関数を提供することにより
def division_function(dividend, divisor):
"""
Dividing two numbers.
:type dividend: int
:type divisor: int
:raises: ZeroDivisionError if divisor is zero (0).
:rtype: int
"""
return dividend / divisor
class MyTestCase(unittest.TestCase):
def test_passing_function(self):
self.assertRaises(ZeroDivisionError, division_function, 1, 0)
チェックする例外は最初のパラメーターでなければならず、呼び出し可能な関数を2番目のパラメーターとして渡す必要があります。指定された他のパラメータは、呼び出される関数に直接渡され、例外をトリガするパラメータを指定できます。
単体テスト内のアサーションの選択
Pythonにはassert
文がありますが、Pythonの単体テストフレームワークは、テストに特化したより優れたアサーションを持っています。失敗に関する情報が豊富で、実行のデバッグモードに依存しません。
おそらく最も単純なアサーションはassertTrue
。これは次のように使用できます:
import unittest
class SimplisticTest(unittest.TestCase):
def test_basic(self):
self.assertTrue(1 + 1 == 2)
これは正常に動作しますが、上記の行を
self.assertTrue(1 + 1 == 3)
失敗するでしょう。
assertTrue
アサーションは、テストされたものが何らかのブール条件としてキャストできるため、最も一般的なアサーションである可能性が高いですが、しばしばより良い選択肢があります。上記と同じようにテストするときは、
self.assertEqual(1 + 1, 3)
前者が失敗すると、メッセージは次のようになります。
======================================================================
FAIL: test (__main__.TruthTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stuff.py", line 6, in test
self.assertTrue(1 + 1 == 3)
AssertionError: False is not true
後者が失敗した場合、メッセージは次のようになります。
======================================================================
FAIL: test (__main__.TruthTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "stuff.py", line 6, in test
self.assertEqual(1 + 1, 3)
AssertionError: 2 != 3
より有益である(実際には左手側の結果を評価した)。
アサーションのリストは、標準のドキュメントにあります 。一般的に、最も具体的に条件に合ったアサーションを選択することは良い考えです。したがって、上記のように、 1 + 1 == 2
であることをassertEqual
は、 assertTrue
assertEqual
よりも使用する方が良いassertTrue
。同様に、 a is None
でa is None
主張するa is None
、 assertIsNone
以外のassertEqual
を使用する方が良いassertEqual
。
アサーションには負の形式があることにも注意してください。したがって、 assertEqual
は負の対応assertNotEqual
があり、 assertIsNone
は負の対応assertIsNotNone
ます。もう一度、必要に応じてネガティブカウンターパートを使用すると、より明確なエラーメッセージが表示されます。
pytestによるユニットテスト
pytestのインストール:
pip install pytest
テストを準備する:
mkdir tests
touch tests/test_docker.py
docker_something/helpers.py
テストする関数:
from subprocess import Popen, PIPE
# this Popen is monkeypatched with the fixture `all_popens`
def copy_file_to_docker(src, dest):
try:
result = Popen(['docker','cp', src, 'something_cont:{}'.format(dest)], stdout=PIPE, stderr=PIPE)
err = result.stderr.read()
if err:
raise Exception(err)
except Exception as e:
print(e)
return result
def docker_exec_something(something_file_string):
fl = Popen(["docker", "exec", "-i", "something_cont", "something"], stdin=PIPE, stdout=PIPE, stderr=PIPE)
fl.stdin.write(something_file_string)
fl.stdin.close()
err = fl.stderr.read()
fl.stderr.close()
if err:
print(err)
exit()
result = fl.stdout.read()
print(result)
テストはtest_docker.py
インポートしtest_docker.py
:
import os
from tempfile import NamedTemporaryFile
import pytest
from subprocess import Popen, PIPE
from docker_something import helpers
copy_file_to_docker = helpers.copy_file_to_docker
docker_exec_something = helpers.docker_exec_something
test_docker.py
オブジェクトのようなファイルを嘲笑する:
class MockBytes():
'''Used to collect bytes
'''
all_read = []
all_write = []
all_close = []
def read(self, *args, **kwargs):
# print('read', args, kwargs, dir(self))
self.all_read.append((self, args, kwargs))
def write(self, *args, **kwargs):
# print('wrote', args, kwargs)
self.all_write.append((self, args, kwargs))
def close(self, *args, **kwargs):
# print('closed', self, args, kwargs)
self.all_close.append((self, args, kwargs))
def get_all_mock_bytes(self):
return self.all_read, self.all_write, self.all_close
でpytestとパッチ適用モンキーtest_docker.py
:
@pytest.fixture
def all_popens(monkeypatch):
'''This fixture overrides / mocks the builtin Popen
and replaces stdin, stdout, stderr with a MockBytes object
note: monkeypatch is magically imported
'''
all_popens = []
class MockPopen(object):
def __init__(self, args, stdout=None, stdin=None, stderr=None):
all_popens.append(self)
self.args = args
self.byte_collection = MockBytes()
self.stdin = self.byte_collection
self.stdout = self.byte_collection
self.stderr = self.byte_collection
pass
monkeypatch.setattr(helpers, 'Popen', MockPopen)
return all_popens
テストの例は、 test_docker.py
ファイルの接頭辞test_
で始まる必要があります。
def test_docker_install():
p = Popen(['which', 'docker'], stdout=PIPE, stderr=PIPE)
result = p.stdout.read()
assert 'bin/docker' in result
def test_copy_file_to_docker(all_popens):
result = copy_file_to_docker('asdf', 'asdf')
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert mock_read
assert result.args == ['docker', 'cp', 'asdf', 'something_cont:asdf']
def test_docker_exec_something(all_popens):
docker_exec_something(something_file_string)
collected_popen = all_popens.pop()
mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
assert len(mock_read) == 3
something_template_stdin = mock_write[0][1][0]
these = [os.environ['USER'], os.environ['password_prod'], 'table_name_here', 'test_vdm', 'col_a', 'col_b', '/tmp/test.tsv']
assert all([x in something_template_stdin for x in these])
一度に1つずつテストを実行する:
py.test -k test_docker_install tests
py.test -k test_copy_file_to_docker tests
py.test -k test_docker_exec_something tests
tests
フォルダ内のすべてのテストを実行します。
py.test -k test_ tests