IT/파이썬 기초 완전 정복

파이썬 단위 테스트 기초 (28)

지식 발전소 2024. 4. 22. 10:06
728x90
반응형

1

단위 테스트란 무엇인가

안녕하세요? 이번에는 파이썬 프로그래밍에서 매우 중요한 단위 테스트에 대해 알아보겠습니다. 단위 테스트란 무엇일까요? 단위 테스트는 소프트웨어의 개별 단위(보통 함수나 메서드)가 의도된 대로 작동하는지 검증하는 과정입니다. 프로그램 전체가 아닌 작은 단위를 테스트하기 때문에 단위 테스트라고 부릅니다.

단위 테스트를 수행하는 이유는 코드의 품질을 보장하고 버그를 조기에 발견하기 위함입니다. 처음부터 완벽한 코드를 작성하기란 쉽지 않습니다. 하지만 단위 테스트를 통해 코드의 오류를 지속적으로 점검하고 수정할 수 있습니다. 결과적으로 안정적이고 유지보수 가능한 코드를 만들 수 있습니다.

또한 단위 테스트는 코드 리팩토링의 안전성을 보장해줍니다. 코드를 개선하거나 기능을 추가할 때, 기존 기능이 제대로 작동하는지 단위 테스트를 통해 확인할 수 있습니다. 이렇게 테스트 주도 개발(TDD)을 하면 훨씬 안전하게 코드를 개선할 수 있습니다.

파이썬에는 단위 테스트를 위한 다양한 프레임워크가 있지만, 그 중에서도 unittest 모듈이 가장 많이 사용됩니다. 이어지는 장에서는 unittest를 중심으로 단위 테스트 작성법을 배워보겠습니다.

unittest 모듈로 단위 테스트 작성하기

파이썬 unittest 모듈은 단위 테스트를 작성하고 실행하는 기본적인 방법을 제공합니다. unittest를 사용하면 테스트 케이스와 테스트 메서드를 쉽게 작성할 수 있습니다. 그렇다면 unittest로 어떻게 단위 테스트를 작성할까요?

  1. 테스트 케이스 정의하기 테스트 케이스는 unittest.TestCase를 상속받아 정의합니다. 하나의 테스트 케이스 클래스당 관련된 여러 테스트를 작성할 수 있습니다.
import unittest

class TestMathFunc(unittest.TestCase):
    """Math 함수에 대한 테스트 케이스"""

    def test_add(self):
        """덧셈 함수 테스트"""
        self.assertEqual(math.add(3, 4), 7)

    def test_multiply(self):
        """곱셈 함수 테스트"""  
        self.assertEqual(math.multiply(3, 5), 15)
  1. 테스트 메서드 작성하기 테스트 메서드는 테스트 케이스 내에 작성하며, 이름이 'test'로 시작해야 합니다. 테스트 로직은 assert 메서드를 사용하여 작성합니다. 위 예시에서는 assertEqual 메서드를 사용했습니다.
  2. 테스트 실행하기 unittest 모듈의 main() 메서드나 텍스트 러너를 사용하여 테스트를 실행할 수 있습니다.
if __name__ == '__main__':
    unittest.main()

이렇게 하면 실행 결과가 콘솔에 출력됩니다. 성공한 테스트와 실패한 테스트가 표시됩니다.

unittest에는 이 밖에도 더 많은 기능이 있습니다. 테스트 고립화를 위한 setUp(), tearDown() 메서드, 테스트 생략을 위한 skip 데코레이터, 예외 테스트를 위한 assertRaises() 메서드 등을 제공합니다.

import unittest

class TestMathFunc(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        """모든 테스트 전에 한 번 실행됩니다."""
        print('setUpClass() 실행')

    def setUp(self):
        """각 테스트 전에 실행됩니다."""
        print('setUp() 실행')

    @unittest.skip("나중에 수정 필요")
    def test_add(self):
        """덧셈 함수 테스트"""
        self.assertEqual(math.add(3, 4), 7)

    def test_divide(self):
        """0으로 나누면 예외가 발생합니다."""
        self.assertRaises(ValueError, math.divide, 5, 0)

    def tearDown(self):
        """각 테스트 후에 실행됩니다."""
        print('tearDown() 실행')

    @classmethod  
    def tearDownClass(cls):
        """모든 테스트 후에 한 번 실행됩니다."""
        print('tearDownClass() 실행')

이처럼 unittest를 사용하면 다양한 상황에 대한 체계적인 테스트가 가능합니다. 이제 단위 테스트를 작성하는 방법을 익혔으니, 다음 장에서는 테스트 주도 개발에 대해 알아보겠습니다.

테스트 주도 개발 (TDD)

테스트 주도 개발(Test-Driven Development, TDD)이란 무엇일까요? TDD는 코드 작성 전에 테스트 케이스를 먼저 작성하고, 해당 테스트를 통과하는 코드를 작성하는 반복적인 개발 프로세스입니다. 즉, 테스트가 코드 작성을 주도합니다.

TDD 프로세스는 레드(실패) -> 그린(성공) -> 리팩토링의 주기로 이루어집니다.

  1. 레드(Red): 테스트 케이스를 작성합니다. 이 때 테스트는 실패해야 합니다.
  2. 그린(Green): 테스트를 통과하는 최소한의 코드를 작성합니다.
  3. 리팩토링(Refactor): 중복 제거, 가독성 개선 등을 통해 코드를 개선합니다.

이렇게 작은 단계를 밟아가며 테스트와 코드를 함께 개발합니다. TDD를 따르면 다음과 같은 이점이 있습니다.

  1. 버그 예방: 테스트 케이스가 코드의 스펙을 정의하므로 버그가 생길 확률이 낮아집니다.
  2. 코드 설계 개선: 테스트 코드를 작성하면서 코드의 설계를 고민하게 됩니다.
  3. 문서화 효과: 테스트 코드는 코드의 사용법을 잘 설명해줍니다.
  4. 리팩토링 안전성: 기능 테스트가 존재하므로 코드 변경 시 안전성이 보장됩니다.

TDD는 초기에는 생산성이 떨어질 수 있지만, 시간이 지날수록 오히려 생산성이 높아지는 장점이 있습니다. 버그가 줄어들고 코드가 깔끔해지기 때문입니다. 초기 비용 대비 장기적으로 얻을 수 있는 이익이 크므로, TDD를 꾸준히 실천하는 것이 좋습니다.

TDD 예시는 다음과 같습니다.

# 1. 테스트 케이스 작성 (실패 케이스)
def test_sum_list():
    data = [1, 3, 5]
    assert sum_list(data) == 9

def sum_list(data):
    pass 

if __name__ == "__main__":
    test_sum_list()
    print("실패!") # 실패 출력

# 2. 최소한의 코드 작성 (통과 케이스)  
def sum_list(data):
    total = 0
    for x in data:
        total += x
    return total

if __name__ == "__main__":    
    test_sum_list()
    print("성공!") # 성공 출력

# 3. 리팩토링
def sum_list(data):
    return sum(data)

# 반복 ...

이렇게 TDD 사이클을 반복하면서 점진적으로 기능을 추가하고 코드를 개선해 나갑니다. 단위 테스트와 테스트 주도 개발을 병행하면 좋은 품질의 파이썬 코드를 작성할 수 있습니다.

목(Mock) 객체를 이용한 테스팅

때로는 직접 객체를 사용하기 어려운 상황이 있습니다. 예를 들어 외부 API를 호출하거나 데이터베이스에 접근해야 할 때입니다. 이런 상황에서는 목(Mock) 객체를 사용하여 테스트합니다.

목 객체란 무엇일까요? 목 객체는 실제 객체의 가짜 구현체로, 테스트 중에만 사용되는 임시 객체입니다. 테스트할 코드에서 실제 객체 대신 목 객체를 사용하면, 실제 객체를 호출하지 않고도 테스트할 수 있습니다.

주로 unittest.mock 모듈을 사용하여 목 객체를 만듭니다.

import unittest
from unittest.mock import Mock, patch

class TestSomeCode(unittest.TestCase):
    
    @patch('urllib.request')
    def test_call_api(self, mock_urllib):
        """API 호출 테스트"""
        mock_response = Mock()
        mock_response.getcode.return_value = 200
        mock_urllib.urlopen.return_value = mock_response

        # 실제 urllib.request 대신 목 객체를 사용
        resp = call_api()  

        self.assertTrue(resp)

위 예시에서 patch 데코레이터는 모듈의 일부를 목 객체로 대체해줍니다. 그리고 Mock 클래스로 목 객체의 반환 값을 지정할 수 있습니다.

이렇게 Mock 객체를 사용하면 다음과 같은 이점이 있습니다.

  • 테스트 실행 속도가 빨라집니다. 실제 객체를 호출할 필요가 없기 때문입니다.
  • 테스트 환경을 제어할 수 있습니다. 다양한 반환 값을 지정해 여러 시나리오를 테스트할 수 있습니다.
  • 부수 효과(side effect)가 없습니다. 목 객체는 단순히 반환 값만 주기 때문에 부수 효과가 없습니다.

단, 목 객체를 과도하게 사용하면 테스트 코드가 실제 코드와 동떨어질 수 있으니 주의해야 합니다.

Mock을 자주 사용하게 되는 상황은 무엇일까요? 데이터베이스나 외부 API를 사용할 때, 시간이 오래 걸리는 작업을 테스트할 때, 특정 예외 상황을 재현하기 어려울 때 등이 있습니다.

실제 예제를 통해 Mock 사용법을 더 자세히 알아보겠습니다.

# requests 라이브러리를 테스트할 때 mock을 사용한 예시
import requests
from unittest import TestCase
from unittest.mock import patch, Mock

def get_data():
    response = requests.get('https://api.example.com/data')
    if response.status_code == 200:
        return response.json()
    else:
        return None

class TestGetData(TestCase):

    @patch('requests.get')
    def test_get_data(self, mock_get):
        # requests.get이 호출되면 mock_response를 반환하도록 설정
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {'data': 'test data'}
        mock_get.return_value = mock_response

        # get_data 함수 호출
        data = get_data()

        # 결과 확인
        self.assertIsNotNone(data)
        self.assertEqual(data, {'data': 'test data'})

이렇게 목 객체를 사용하면 실제 API를 호출하지 않고도 테스트할 수 있습니다. 여러분의 코드에서 유사한 상황이 있다면 적극 활용해보시기 바랍니다.

코드 커버리지 측정하기

마지막으로 코드 커버리지(Code Coverage) 개념에 대해 알아보겠습니다. 코드계속해서 코드 커버리지에 대해 살펴보겠습니다.

코드 커버리지란 무엇일까요? 코드 커버리지는 소스 코드의 얼마나 많은 부분이 테스트에 의해 실행되었는지를 보여주는 지표입니다. 다시 말해, 테스트 커버리지가 높다는 것은 많은 코드가 테스트에 의해 검증되었음을 의미합니다.

코드 커버리지는 일반적으로 라인 커버리지, 브랜치 커버리지, 조건 커버리지 등으로 구분됩니다.

  1. 라인 커버리지(Line Coverage): 코드 라인 수 대비 실행된 라인 수의 비율입니다.
  2. 브랜치 커버리지(Branch Coverage): 조건문의 모든 분기가 실행되는지를 나타냅니다.
  3. 조건 커버리지(Condition Coverage): 조건식의 모든 조건이 참과 거짓을 충족하는지를 나타냅니다.

일반적으로 100%의 커버리지를 달성하기는 어렵지만, 코드 커버리지를 높일수록 버그를 조기에 발견할 확률이 높아집니다.

파이썬에서 코드 커버리지를 측정하려면 coverage.py 패키지를 사용하면 됩니다. 설치는 pip로 간단히 할 수 있습니다.

$ pip install coverage

그리고 coverage 명령으로 측정하고 싶은 모듈이나 패키지를 지정하면 커버리지 리포트를 생성할 수 있습니다.

$ coverage run -m unittest discover tests/
$ coverage report

이렇게 하면 터미널에 커버리지 요약 리포트가 출력됩니다. 자세한 리포트를 HTML로 생성하려면 다음과 같이 실행합니다.

$ coverage html

그러면 htmlcov 디렉터리에 HTML 리포트 파일이 생성됩니다. 브라우저로 열어보면 테스트 커버리지에 대한 구체적인 내용을 볼 수 있습니다.

코드 커버리지는 소프트웨어 품질을 높이기 위한 지표일 뿐, 100% 달성이 목표가 되어서는 안 됩니다. 상황에 맞추어 적절한 커버리지 목표치를 정하고, 그에 따라 테스트를 지속적으로 보완해 나가는 것이 바람직합니다.

참고 자료

[1] 웨슬리 촐락 "Python 단위 테스트 철학" (2014) [2] 브렌트 필링 "Python 단위 테스트 실행기" (2017)
[3] 데이빗 힌슨 "Python 팩토리" (2022)

 

 

한 고대 문서 이야기

여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오

gospel79.tistory.com

 

유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법

올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을

stock79.tistory.com

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90
반응형