수색…


비고

Python을위한 몇 가지 단위 테스트 도구가 있습니다. 이 문서에서는 기본 unittest 모듈에 대해 설명합니다. 다른 테스트 도구에는 py.testnosetests 있습니다. 테스트에 관한파이썬 문서 는 이러한 도구 중 몇 가지를 깊이 비교하지 않고 비교합니다.

예외 테스트

예를 들어 잘못된 입력이 주어지면 프로그램에서 오류가 발생합니다. 이 때문에 실제 잘못된 입력이 주어지면 오류가 발생하는지 확인해야합니다. 따라서 정확한 예외를 확인해야합니다.이 예제에서는 다음 예외를 사용합니다.

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 는 두 가지 방법으로 사용할 수 있습니다.

  1. 일반 함수 호출 사용. 첫 번째 인수는 예외 유형을, 두 번째는 호출 가능 (대개 함수)을 취하고 나머지 인수는이 호출 가능 함수에 전달됩니다.
  2. with 절을 사용하여 예외 유형 만 함수에 제공합니다. 이것은 더 많은 코드가 실행될 수 있다는 장점이 있지만 여러 함수가 문제가 될 수있는 동일한 예외를 사용할 수 있으므로주의해서 사용해야합니다. 예 : self.assertRaises (WrongInputException) : convert2number ( "숫자가 아님")

이것은 먼저 다음 테스트 케이스에서 구현되었습니다.

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()

또한 던져서는 안되는 예외를 검사 할 필요가있을 수 있습니다. 그러나 예외가 발생하면 테스트가 자동으로 실패하므로 전혀 필요하지 않을 수 있습니다. 옵션을 보여 주기만하면 두 번째 테스트 메소드는 던져지지 않는 예외를 검사 할 수있는 방법에 대한 사례를 보여줍니다. 기본적으로 이것은 예외를 포착하고 fail 메소드를 사용하여 테스트를 fail 합니다.

unittest.mock.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 을 조롱함으로써 multiples_of 만 테스트 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 내의 테스트 설치 및 해체

때로는 각 테스트가 실행될 컨텍스트를 준비하려고합니다. setUp 메소드는 클래스의 각 테스트 이전에 실행됩니다. tearDown 은 모든 테스트가 끝날 때마다 실행됩니다. 이 메소드는 선택 사항입니다. TestCases는 협동 적 다중 상속에서 자주 사용되므로 기본 클래스의 setUptearDown 메서드도 호출되도록이 메서드에서 항상 super 를주의해야합니다. TestCase 의 기본 구현은 빈 setUptearDown 메서드를 제공하므로 예외를 발생시키지 않고 호출 할 수 있습니다.

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 과 달리 addCleanup 을 통해 등록 된 함수는 setUp 에서 처리되지 않은 예외가 발생한 경우에도 호출됩니다. 구체적인 예로서,이 방법은 테스트가 실행되는 동안 등록 된 다양한 모의 객체를 제거하는 것으로 자주 볼 수 있습니다.

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)

이 방법으로 정리를 등록하는 또 다른 이점은 프로그래머가 설치 코드 옆에 정리 코드를 넣을 수 있고 하위 분류자가 tearDown 에서 super 를 호출하는 것을 잊어 버리는 경우 사용자를 보호 할 수 있다는 것입니다.

예외에 대한 주장

함수가 두 가지 방법을 통해 내장 된 unittest를 사용하여 예외를 throw하는지 테스트 할 수 있습니다.

컨텍스트 관리자 사용

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)

확인해야 할 예외는 첫 번째 매개 변수 여야하며 호출 가능한 함수는 두 번째 매개 변수로 전달되어야합니다. 지정된 다른 매개 변수는 호출중인 함수에 직접 전달되어 예외를 트리거하는 매개 변수를 지정할 수 있습니다.

Unittest 내의 어설 션 선택

파이썬에는 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 . 비슷하게, a is None 이라는 것을 주장 할 a is None , assertEqual 보다 assertIsNone 을 사용하는 것이 낫다.

단정문에는 부정적인 형식이 있음에 유의하십시오. 따라서 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

test_docker.py에서 test_docker.py Monkey 패칭하기 :

@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])

한 번에 하나씩 테스트 실행 :

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