サーチ…


備考

Pythonにはいくつかのユニットテストツールがあります。このドキュメントのトピックでは、基本的なunittestモジュールについて説明します。他のテストツールには、 py.testnosetestsます。 テストに関するこの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つの方法で使用できます。

  1. 通常の関数呼び出しを使用します。最初の引数は例外型をとり、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 Nonea is None主張するa is NoneassertIsNone以外の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


Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow