본문 바로가기

카테고리 없음

[Python] unittest 모듈을 이용한 단위 함수 테스트 자동화

(본 포스팅은 Python Crash Course 책 예제코드를 참조했습니다.)

작은 프로젝트의 경우에는 필요할 때 수동으로 테스트를 해도 되지만, 프로젝트의 크기가 커지고 테스트 해봐야 할 양이 많아지면 테스트를 하는 스크립트를 별도로 작성하여 자동화할 필요가 있습니다. 파이썬에서는 이를 위한 모듈이 있습니다. 바로 unittest인데요. unit test란 함수의 특정 기능이 문제 없이 작동하는지 테스트하는 것을 뜻하고 unittest 모듈은 그 역할을 해주는 built-in 모듈입니다. 이 모듈을 사용해 함수가 원하는 바대로 잘 작동하는지 테스트를 수행해보겠습니다.

우선 테스트 대상 함수부터 만들겠습니다.

# name_function.py
def get_formatted_name(first, last):
    """Generate a neatly formatted full name."""
    full_name = first + ' ' + last
    return full_name.title()

위 함수는 이름과 성을 받아, 성명을 출력해주는 함수입니다. 마지막 full_name.title()full_name string을 영어로 된 제목처럼 단어의 앞글자를 대문자로 바꿔줍니다(ex. 'i like python' -> 'I Like Python').

자, 그럼 이제 unittest 모듈을 이용해 이 함수가 올바르게 작동하는지 테스트해봅시다. test는 python script 파일을 별도로 만들어 실행해야 하므로 우선 위 함수를 name_function.py로 저장하고 test script 파일을 test_name_function.py로 새로 만듭니다.

# test_name_function.py
import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """Tests for get_formmatted_name()."""
    def test_first_last_name(self):
        """Do names like 'Janis Joplin' work?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

unittest.main()

여기서는 get_formatted_name(first, last) 함수가 first=janis, last=joplin이라는 argument에 대해 문제 없이 작동하는지 확인합니다. 우선 unittest 모듈을 import하고 unittest 모듈 안에 있는 TestCase class를 상속받아 테스트를 위한 새로운 class를 만듭니다. class 이름은 어떤 걸 사용하셔도 상관없습니다.

그리고 class 안에 단위 기능을 테스트 하기 위한 테스트 함수(method)를 만듭니다. 이때 테스트 함수 이름은 반드시 'test_'라는 단어로 시작해야 합니다.

스크립트를 찬찬히 살펴보시면 상당히 단순합니다. get_formatted_name 함수에 예시 argument를 넣고 assertEqual이라는 unittest.TestCase class에 내장된 method를 이용해 결과가 기대하는 바와 같은지 체크합니다.

아니 도대체 이렇게 단순한 체크를 굳이 함수로 만들어 테스트할 필요가 있을까 하는 의문이 들지 않나요? 배보다 배꼽이 더 큰 작업 같기도 하구요. 뭐 대단한 자동화가 있을 줄 알았는데 결국 직접 다 써야하니 뭐가 편한지도 솔직히 잘 모르겠습니다.

그런데, 테스트 코드를 만드는 목적은 방금 새로 작성한 함수를 테스트하기 위한 것이 아닙니다. 그건 그냥 돌려보고 확인해보면 되지요. 테스트 코드는 차후 유지보수에 필요합니다. 훗날 함수를 수정하여 다른 기능을 조금 추가했을 때 기존에 있던 기능이 잘 작동하는지 체크해야하죠. 만약 이렇게 테스트 코드를 따로 만들어놓지 않는다면 기존 코드의 수정이 예상치 못한 새로운 문제를 만들었을 때 미리 알아채지 못할 것입니다. 문제가 발생하고 나서 원인을 찾기 위해 하나씩 테스트 해야 한다면 유지보수는 지옥이 되겠죠.

자 그럼 unittest 모듈의 진짜 능력을 살펴보겠습니다. 위 테스트 코드(test_name_function.py)를 실행하면 어떻게 될까요? 앞서 테스트 함수는 반드시 'test_'로 시작해야 된다고 말씀드렸죠? uniitest 모듈은 알아서 'test_'로 시작하는 함수들을 다 실행시킵니다. 그리고 결과를 아래와 같이 보여줍니다.

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

가장 위에 ' . '은 오타가 아니라 테스트 함수 하나가 무사히 통과했다는 의미이고, 마지막 OK는 모든 테스트 함수를 통과했다는 의미입니다.

그럼 함수를 수정하는 상황을 가정한 뒤 다시 테스트 해보겠습니다.

# name_function.py
def get_formatted_name(first, middle, last):
    """Generate a neatly formatted full name."""
    full_name = first + ' ' + middle + ' ' + last
    return full_name.title()

middle name을 받아들이는 함수 형태로 바꾸었습니다. 다시 돌려보겠습니다.

E
======================================================================
ERROR: test_first_last_name (__main__.NamesTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:/Users/Jeonghoon/PycharmProjects/unittest/test_name_function.py", line 6, in test_first_last_name
    formatted_name = get_formatted_name('janis', 'joplin')
TypeError: get_formatted_name() missing 1 required positional argument: 'last'

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)

' . ' 대신 'E'가 찍혔고 어디서 왜 에러가 났는지 알려줍니다. 함수를 수정하면서 기존에 테스트를 통과했던 기능이 에러가 난 것이죠. 만약 테스트 통과에 실패한다면 원래의 함수로 돌아가 문제가 된 부분을 고쳐야겠죠.

def get_formatted_name(first, last, middle=''):
    """Generate a neatly formatted full name."""
    if middle:
        full_name = first + ' ' + middle + ' ' + last
    else:
        full_name first + ' ' + last
    return full_name.title()

middle 함수를 키워드 인자(key=value 형태로 함수의 인자를 넘기는 법. 이때 해당 인자는 굳이 쓰지 않아도 되는 optional 인자가 됩니다)로 받아들이고 default 값은 빈 string으로 설정했습니다. 만약 middle이 빈 string이 아니면 middle name까지 출력하고, 빈 string이면 first name과 last name만 출력합니다.

그럼 이제 middle name이 있을 때와 없을 때 각각 잘 작동하는지 테스트 함수로 확인해보겠습니다.

# test_name_function.py
import unittest
from name_function import get_formatted_name

class NamesTestCase(unittest.TestCase):
    """Tests for get_formmatted_name()."""
    def test_first_last_name(self):
        """Do names like 'Janis Joplin' work?"""
        formatted_name = get_formatted_name('janis', 'joplin')
        self.assertEqual(formatted_name, 'Janis Joplin')

    def test_first_last_middle_name(self):
        """Do names like 'Wolfgang Amadeus Mozart' work?"""
        formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus')
        self.assertEqual(formatted_name, 'Wolfgang Amadeus Mozart')

unittest.main()

테스트 결과는 아래와 같습니다.

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

점 ' . '이 두개가 되었죠?('..') 가장 첫줄에 출력되는 값은 테스트 결과를 요약해줍니다. 만약 첫 번째 테스트 함수만 통과하고 두 번째 테스트 함수는 통과하지 못했다면 '.E'와 같이 찍혔을 것입니다. 테스트를 하는 사람은 테스트 실패한 함수가 대충 어디쯤에 위치하는지 확인할 수 있죠. 수십개 수백개의 테스트 함수가 있다면 유용할 것입니다.

앞서 assertEqual(a, b) method를 사용했지만 이외에도 다른지를 확인하는 assertNotEqual(a, b), item이 list안에 있는지 확인하는 assertIn(item, list)등 다양한 체크 함수가 있습니다. 더 자세한 내용은 파이썬 매뉴얼 페이지에서 확인할 수 있습니다.

가장 단순한 예시를 들었는데 어떻게 사용하는지 감이 오시나요?

unit test는 가장 기본적인 테스트 중 하나입니다. 유지보수의 지옥에 빠지지 않으려면 테스트 함수를 함께 만드는 습관을 들이는 것이 좋습니다.