Original article: An Introduction to Unit Testing in Python

얼마간의 코드 작성을 방금 끝내고나서 무엇을 해야 하는지 궁금할 것입니다. 풀 리퀘스트(pull request)를 제출하고 팀원들에게 코드 리뷰를 받을 것인가요? 또는 일일이 코드를 모두 테스트 해 볼 것인가요?

두 가지 사항 모두 반드시 해야 합니다만 추가 단계가 하나 더 있습니다: 작성한 코드에 유닛 테스트를 해서 코드가 작성된 의도대로 동작하는지 확실히 해야 합니다.

유닛 테스트는 통과 또는 실패할 수 있어서, 그 자체로 코드를 점검하는 훌륭한 기법입니다. 이번 튜토리얼에서는 파이썬으로 유닛 테스트를 작성하는 방법을 보여줄텐데 그러면 여러분은 스스로의 프로젝트에 이것을 적용하는게 얼마나 쉬운지 알게 될 것입니다.

시작하기

테스팅을 이해하는 가장 좋은 방법은 스스로 실험해보는 것입니다. 그러한 목적에 맞춰 name_function.py라는 파일 안에 간단한 함수를 작성하여 이름과 성을 가져오고 풀네임을 반환하도록 할 것입니다.

#Generate a formatted full name
def formatted_name(first_name, last_name):
   full_name = first_name + ' ' + last_name
   return full_name.title()

formatted_name() 함수는 이름과 성을 가져와 풀 네임을 형성하기 위해 두 단어 사이 빈칸을 두고 결합합니다. 그리고나서 모든 단어의 첫번째 문자를 대문자로 변환합니다. 이 코드가 작동하는지 확인하려면 이 함수 안에 사용할 몇 가지 코드를 작성해야 합니다. names.py 안에 몇 가지 간단한 코드를 작성해서 사용자들로부터 그들의 이름과 성을 입력하도록 하게끔 할 것입니다:

from name_function import formatted_name

print("Please enter the first and last names or enter x to E[x]it.")

while True:
   first_name = input("Please enter the first name: ")
   if first_name == "x":
       print("Good bye.")
       break

   last_name = input("Please enter the last name: ")
   if last_name == "x":
       print("Good bye.")
       break

   result = formatted_name(first_name, last_name)
   print("Formatted name is: " + result + ".")

이 코드는 name_function.py에서 formatted_name()를 불러와서 실행하고 사용자에게 성과 이름을 연달아 입력하게끔 허용한 다음 풀네임을 형성하여 보여줍니다.

유닛 테스트와 테스트 케이스

파이썬 표준 라이브러리에는 unittest라고 불리는 모듈이 있어서 코드를 테스팅하는 도구를 포함하고 있습니다. 유닛 테스트는 모든 기능의 모든 특정 부분 동작이 올바른지 확인하고 나머지 부분을 훨씬 쉽게 통합합니다.

테스트 케이스는 하나의 기능이 의도한대로 동작하는지, 기능이 스스로를 잘 찾아내고 상황을 다룰 수 있을 것이라 예상된 전체 범위의 상황 속에서 잘 동작한다고 증명하는 유닛 테스트의 컬렉션(collection)입니다. 테스트 케이스는 어떤 함수가 사용자로부터 받을 수 있는 모든 가능한 종류의 입력을 고려해야만 하고, 그러므로 그러한 상황 각각을 대표하는 테스트를 포함해야만 합니다.

테스트 진행

테스트를 작성하는 통상적인 시나리오들입니다:

우선 테스트 파일을 하나 생성해야 합니다. 그리고나서 unittest 모듈을 불러와 unittest.TestCase로부터 상속받는 테스팅 클래스를 정의하고, 마지막으로 함수의 행동에 대한 모든 경우를 테스트하는 일련의 메소드들을 작성합니다.

아래 코드를 한 줄씩 설명하겠습니다:

import unittest
from name_function import formatted_name

class NamesTestCase(unittest.TestCase):

   def test_first_last_name(self):
       result = formatted_name("pete", "seeger")
       self.assertEqual(result, "Pete Seeger")

첫번째로 unittest와 테스트 하고 싶은 함수, formatted_name()을 불러와야 합니다. 그리고나서 클래스 하나를 생성하는데 예를 들어 NamesTestCase라고 부르는 함수를 우리의 formatted_name() 기능에 대한 테스트를 포함하는 것이 될 수 있습니다. 이 클래스는 unittest.TestCase 클래스에서 상속받습니다.

NamesTestCase는 formatted_name()의 한 부분을 테스트하는 하나의 메소드를 포함하고 있습니다. 이 메소드를 test_first_last_name()이라 부를 수 있습니다.

"test_"로 시작하는 모든 메소드들이 test_name_function.py를 실행할 때 자동으로 실행될 것을 기억하세요.

test_first_last_name() 테스트 메소드 안에서, 테스트를 하고 싶은 함수를 부르고 반환 값을 저장합니다. 이 예제에서 우리는 formatted_name()을 "pete", "seeger"라는 인수(arguments)로 호출할 것이며 반환 값을 결과에 저장할 것입니다.

마지막 줄에서 우리는 선언 메소드를 사용할 것입니다. 선언 메소드는 우리가 받은 결과가 예상했던 결과와 일치하는지 검증합니다. 그리고 이 케이스에서 우리는 formatted_name() 함수가 각각의 첫번째 글자가 대문자로 지정된 풀네임을 받을 것을 알고 있고, 그래서 우리는 "Pete Seeger"라는 결과를 예상합니다. 이를 확인하기 위해서 unittest의 assertEqual() 메소드가 사용되었습니다.

self.assertEqual(result, “Pete Seeger”)

해당 라인은 기본적으로 의미하길: 결과 값을 "Pete Seeger"와 비교하고 만약 그 둘이 동일하면 OK, 일치하지 않으면 알려달라는 것을 뜻합니다.

test_name_function.py를 실행하면서 테스트가 통과했다는 OK 사인을 받을 것을 예상합니다.

Ran 1 test in 0.001s

OK

테스트 실패

테스트에 실패하는게 어떤 것인지 보여주기 위해 formatted_name() 함수를 새로운 미들 네임 매개변수를 포함하는 것으로 수정하겠습니다.

그래서 이와 같은 함수를 다시 작성합니다:

#Generate a formatted full name including a middle name
def formatted_name(first_name, last_name, middle_name):
   full_name = first_name + ' ' + middle_name + ' ' + last_name
   return full_name.title()

해당 버전의 formatted_name()은 미들 네임을 가진 사람들을 위해 작동할 것이지만 테스트 할 때 함수가 미들 네임이 없는 사람들 대상으로는 동작하지 않는 것을 보게 될 것입니다.

그래서 test_name_function.py를 실행할 때 이와 같은 결과 값을 얻을 것입니다:

Error
Traceback (most recent call last):

File “test_name_function.py”, line 7, in test_first_last_name
    result = formatted_name(“pete”, “seeger”)

TypeError: formatted_name() missing 1 required positional argument: ‘middle_name’

Ran 1 test in 0.002s

FAILED (errors=1)

결과 값에서 어떤 부분에 테스트가 실패했는지 알기 위해 필요한 모든 걸 말해주는 정보들을 보게 될 것입니다.

  • 결과 값 첫번째 항목이 Error이며 테스트 케이스 안에 적어도 하나의 테스트가 에러 결과를 냈다는 것을 알려줍니다.
  • 다음으로는 에러가 발생한 파일과 메소드를 보게 될 것입니다.
  • 그 다음으로 에러가 발생한 줄을 보게 될 것입니다.
  • 그리고 어떤 종류의 에러인지, 이번 경우에는 우리가 "middle_name" 매개변수를 찾을 수 없다는 것을 알 수 있습니다.
  • 테스트 실행 횟수를 볼 수 있으며 테스트를 완료하기 위해 필요했던 시간, 그리고 에러가 발생한 횟수와 함께 테스트 상태를 대표하는 문자 메세지를 보게 될 것입니다.

테스트 실패시 해야 하는 일

테스트에 통과한다는 것은 함수가 예상된 대로 동작한다는 뜻입니다. 하지만 테스트에 실패한다는 것은 앞으로 더 재미있는 것이 있다는 걸 뜻합니다.

코드를 개선하는 대신 테스트를 변경하는 걸 선호하는 프로그래머들을 몇몇 봐 왔습니다 - 하지만 그러지 마세요. 문제를 고치는데 약간의 시간을 더 투자하면 코드를 더 잘 이해하게 될 것이고 장기적으로 봤을 때는 결국 시간을 줄이게 될 것입니다.

이번 예제에서 우리의 함수 formatted_name()은 먼저 두 개의 파라미터(parameters)를 요구했고, 이제 그것은 하나의 추가 요소(미들 네임)를 더 요구하는 것으로 재작성 되었습니다. 우리의 함수에 미들 네임을 더하는 것은 그 행동이 바라는 것을 동작하지 않게 합니다. 아이디어가 테스트를 변경하지 않겠다는 것이므로 가장 좋은 해결책은 미들 네임 여부를 선택할 수 있게 만드는 것입니다.

우리가 이 아이디어를 실행하면 예를 들어 "Pete Seeger"처럼 이름과 성이 사용되었을 때 테스트를 통과하게 만들 수 있고, 예를 들어 "Raymond Red Reddington"처럼 이름과 성, 미들네임까지 사용되었을 때도 테스트를 잘 통과하게 할 수 있습니다. 그러니 formatted_name() 코드를 한번 더 수정해 봅시다:

#Generate a formatted full name including a middle name
def formatted_name(first_name, last_name, middle_name=''):
   if len(middle_name) > 0:
       full_name = first_name + ' ' + middle_name + ' ' + last_name
   else:
       full_name = first_name + ' ' + last_name
   return full_name.title()

이제 함수는 미들 네임이 있는 이름과 없는 이름 모두에게 동작할 것입니다.

그리고 확실히 하기 위해 "Pete Seeger"로 여전히 동작하는지 테스트를 다시 실행합니다:

Ran 1 test in 0.001s

OK

그리고 이것이 제가 여러분에게 보여주고자 했던 것입니다. 코드를 테스트에 맞게 고치는 것이 다른 식으로 돌아가는 것보다 항상 더 나은 결과를 보여줍니다. 이제 미들 네임을 가지고 있는 이름들을 위해 새로운 테스트를 더해봅니다.

새로운 테스트 더하기

NamesTestCase 클래스에 미들 네임을 테스트하는 새로운 메소드를 작성합니다:

import unittest
from name_function import formatted_name

class NamesTestCase(unittest.TestCase):

    def test_first_last_name(self):
        result = formatted_name("pete", "seeger")
        self.assertEqual(result, "Pete Seeger")

    def test_first_last_middle_name(self):
        result = formatted_name("raymond", "reddington", "red")
        self.assertEqual(result, "Raymond Red Reddington")

테스트를 실행하면 통과할 것입니다:

Ran 2 tests in 0.001s

OK

Bra gjort!
(역주: 스웨던어) 잘했어요!

여러분은 함수가 미들 네임을 가진 이름 또는 미들 네임을 가지지 않은 이름을 이용해 동작하는지 점검하는 테스트를 작성했습니다. 파이썬 테스팅에 대해 더 많이 이야기할 파트 2를 기대해주세요.

읽어주셔서 감사합니다! 이와 같은 글을 여기 제 프리코드캠프 프로필에서 더 많이 확인해보세요: https://www.freecodecamp.org/news/author/goran/ 그리고 제 깃허브 페이지에 다른 재미있는 것들도 찾아보세요: https://github.com/GoranAviani