코드 테스트하기

../_images/34435687940_8f73fc1fa6_k_d.jpg

코드 테스트는 아주 중요합니다.

Getting used to writing testing code and running this code in parallel is now considered a good habit. Used wisely, this method helps to define your code's intent more precisely and have a more decoupled architecture.

테스트의 일반 원칙 몇 가지:

  • 테스트 유닛은 각 기능의 가장 작은 단위에 집중하여, 해당 기능이 정확히 동작하는지를 증명해야 합니다.
  • 각 테스트 유닛은 반드시 독립적이어야 합니다. 각 테스트는 혼자서도 실행 가능해야하고, 테스트 슈트로도 실행 가능해야 합니다. 이 때, 호출되는 순서와 무관하게 잘 동작해야 합니다. 이 규칙이 뜻하는 바, 새로운 데이터셋으로 각각의 테스트를 로딩해야 하고, 그 실행 결과는 꼭 삭제해야합니다. 보통 setUp()tearDown() 메소드로 이런 작업을 합니다.
  • 테스트가 빠르게 돌 수 있도록 만들기 위해 노력해야 합니다. 테스트 하나가 실행하는데 몇 밀리세컨드 이상의 시간이 걸린다면, 개발 속도가 느려지거나 테스트가 충분히 자주 수행되지 못할 것입니다. 테스트에 필요한 데이터 구조가 너무 복잡하고, 테스트를 하려면 매번 이 복잡한 데이터를 불러와야 해서 테스트를 빠르게 만들 수 없는 경우도 있습니다. 이럴 때는 무거운 테스트는 따로 분리하여 별도의 테스트 슈트를 만들어 두고 스케쥴 작업을 걸어두면 됩니다. 그리고 그 외의 다른 모든 테스트는 필요한 만큼 자주 수행하면 됩니다.
  • 지금 사용하고 있는 툴이 개별 테스트나 테스트 케이스를 어떻게 수행하는지 배우셔야 합니다. 모듈 안에 들어있는 함수를 개발하고 있다면, 그 함수의 테스트를 자주, 가능하다면 코드를 저장할 때마다 자동으로 돌려야 합니다.
  • 그날의 코딩을 시작하기 전에 항상 풀 테스트 슈트를 돌려야 합니다. 끝난 후에도 마찬가지입니다. 이 작업은 당신이 다른 코드를 망가뜨리지 않았다는 더 큰 자신감을 심어줄 것입니다.
  • 모두가 공유하는 저장소에다가 코드를 집어넣기 전에 자동으로 모든 테스트를 수행하도록 하는 훅을 구현하는 것도 좋은 생각입니다.
  • 지금 한창 개발 중인데 그만두고 잠시 다른 일을 해야한다면, 다음에 개발할 부분에다가 일부러 고장난 유닛 테스트를 작성하는 것도 좋은 생각입니다.
  • 코드를 디버깅할 때 가장 먼저 시작할 일은 버그를 찝어내는 새로운 테스트를 작성하는 것입니다. 이런 일이 언제나 가능한 것은 아니지만, 이런 버그 잡이 테스트들이야말로 당신의 프로젝트에서 가장 가치있는 코드 조각이 될 것입니다.
  • 테스트 함수에는 길고 서술적인 이름을 사용하셔야 합니다. 테스트에서의 스타일 안내서는 짧은 이름을 보다 선호하는 다른 일반적인 코드와는 조금 다릅니다. 테스트 함수는 절대 직접 호출되지 않기 때문입니다. 실제로 돌아가는 코드에서는 square() 라든가 심지어 sqr() 조차도 괜찮습니다. 하지만 테스트 코드에서는 test_square_of_number_2(), test_square_negative_number() 같은 이름을 붙여야 합니다. 이런 함수명들은 테스트가 실패할 때나 보입니다. 그러니 반드시 가능한 한 서술적인 이름을 붙여야 합니다.
  • 무언가 잘못되었거나 뜯어고쳐야만 할 경우, 괜찮은 코드에 테스트 셋이 있다면 당신이나 다른 유지보수 담당자들은 오류를 수정하거나 프로그램의 동작을 수정할 때 필시 그 테스트 슈트에 전적으로 의지할 것입니다.
  • 테스트 코드의 또다른 사용 방법은 새로운 개발자들을 위한 안내서로 쓰는 방법입니다. 이미 만들어져 있는 코드에서 작업해야할 경우, 관련 테스트 코드를 돌려보고 읽어보는 것이야말로 가장 좋은 시작점일 경우가 많습니다. 이렇게 테스트 코드를 돌려보면 어느 지점이 문제인지, 수정하기 어려운 곳은 어디일지, 막다른 골목은 어디일지를 발견하게 됩니다. 몇 가지 기능을 추가해야 한다면 가장 먼저 해야할 일은, 그 새로운 기능이 아직 돌아가지 않음을 확인할 수 있는 테스트를 붙여넣는 것입니다.

기본

unittest

unittest 는 파이썬 표준 라이브러리 중 아주 유용한 테스트 모듈입니다. JUnit/nUnit/CppUnit 시리즈와 같은 툴을 써본 사람이라면 unittest 의 API에도 익숙할 것입니다.

테스트 케이스를 만드려면 unittest.TestCase 를 상속받는 하위 클래스를 만들어야 합니다.

import unittest

def fun(x):
    return x + 1

class MyTest(unittest.TestCase):
    def test(self):
        self.assertEqual(fun(3), 4)

파이썬 2.7부터는 unittest도 자체적인 테스트 탐색 매커니즘이 생겼습니다.

Doctest

doctest 모듈은 독스트링 안에 대화형 파이썬 세션처럼 보이는 텍스트가 있는지를 검색한 후, 해당 세션들을 실행하여 텍스트에 써진대로 정확히 동작하는지를 확인합니다.

doctest는 다른 단위 테스트와는 사용 방법이 다릅니다: doctest는 일반적으로 상세하지 않고 특이한 케이스나 회귀 테스트에서의 버그를 잡아내지도 못합니다. 하지만 각 모듈과 그 컴포넌트의 주된 사용법을 알려주는 문서로써는 아주 유용합니다. doctest는 전체 테스트 슈트를 실행할 때마다 자동으로 돌려야 합니다.

함수에서 doctest를 돌리는 간단한 예시:

def square(x):
    """Return the square of x.

    >>> square(2)
    4
    >>> square(-2)
    4
    """

    return x * x

if __name__ == '__main__':
    import doctest
    doctest.testmod()

커맨드 라인에서 python module.py 를 쳐서 해당 모듈을 실행하면 doctest가 실행되고, 무언가 doctest에 기술한대로 동작하지 않는 경우에는 경고를 해줍니다.

도구

py.test

py.test는 보일러플레이트가 없는 파이썬 표준 unittest의 대체품입니다.

$ pip install pytest

모든 기능을 갖추지는 않았습니다. 확장 가능한 테스트 도구도 아닙니다. 하지만 단순한 신텍스를 자랑합니다. 테스트 슈트를 만드는 것은 함수 몇 개가 있는 모듈 하나를 작성하는 것 만큼이나 간단합니다.

# content of test_sample.py
def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5

and then running the py.test command:

$ py.test
=========================== test session starts ============================
platform darwin -- Python 2.7.1 -- pytest-2.2.1
collecting ... collected 1 items

test_sample.py F

================================= FAILURES =================================
_______________________________ test_answer ________________________________

    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test_sample.py:5: AssertionError
========================= 1 failed in 0.02 seconds =========================

unittest 모듈로 같은 기능을 구현하기 위해 필요한 것보다 훨씬 덜 작업해도 됩니다!

Hypothesis

Hypothesis is a library which lets you write tests that are parameterized by a source of examples. It then generates simple and comprehensible examples that make your tests fail, letting you find more bugs with less work.

$ pip install hypothesis

For example, testing lists of floats will try many examples, but report the minimal example of each bug (distinguished exception type and location):

@given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1))
def test_mean(xs):
    mean = sum(xs) / len(xs)
    assert min(xs) <= mean(xs) <= max(xs)
Falsifying example: test_mean(
    xs=[1.7976321109618856e+308, 6.102390043022755e+303]
)

Hypothesis is practical as well as very powerful and will often find bugs that escaped all other forms of testing. It integrates well with py.test, and has a strong focus on usability in both simple and advanced scenarios.

tox

tox is a tool for automating test environment management and testing against multiple interpreter configurations.

$ pip install tox

tox allows you to configure complicated multi-parameter test matrices via a simple INI-style configuration file.

mock

unittest.mock 은 파이썬의 테스트 라이브러리입니다. 파이썬 3.3부터는 표준 라이브러리 가 되었습니다.

예전 버전의 파이썬에서:

$ pip install mock

를 하면 mock 오브젝트로 시스템의 테스트 파트를 변경할 수 있습니다. 그리고 테스트가 어떻게 쓰일지에 대한 단정문을 만들 수 있습니다.

이렇게 메소드를 몽키 패치할 수도 있습니다:

from mock import MagicMock
thing = ProductionClass()
thing.method = MagicMock(return_value=3)
thing.method(3, 4, 5, key='value')

thing.method.assert_called_with(3, 4, 5, key='value')

테스트하는 모듈에서 모의 클래스나 모의 객체를 만들 경우에는 patch 데코레이터를 사용하세요. 아래의 예시에서는 언제나 같은 결과값을 내는(아무튼 테스트 중에는) 모의 객체가 외부 검색 시스템 하나 대신 쓰였습니다.

def mock_search(self):
    class MockSearchQuerySet(SearchQuerySet):
        def __iter__(self):
            return iter(["foo", "bar", "baz"])
    return MockSearchQuerySet()

# SearchForm here refers to the imported class reference in myapp,
# not where the SearchForm class itself is imported from
@mock.patch('myapp.SearchForm.search', mock_search)
def test_new_watchlist_activities(self):
    # get_search_results runs a search and iterates over the result
    self.assertEqual(len(myapp.get_search_results(q="fish")), 3)

Mock has many other ways with which you can configure and control its behaviour.