Created at : 2024-09-03 15:02
Auther: Soo.Y
11. 테스트와 디버깅 (Testing and Debugging)
소프트웨어 개발에서 테스트와 디버깅은 코드의 품질을 보장하고, 예상치 못한 오류를 발견하여 수정하는 중요한 과정입니다. 이 장에서는 파이썬에서 단위 테스트를 수행하는 방법, 테스트 주도 개발(TDD)의 기초 개념, 디버깅 도구의 활용, 그리고 프로파일링과 코드 최적화 기법을 학습합니다.
11.1 단위 테스트 (Unit Testing)
단위 테스트는 프로그램의 가장 작은 단위인 함수나 메서드가 의도한 대로 작동하는지 확인하는 테스트입니다. 파이썬에서는 unittest
와 pytest
와 같은 도구를 사용하여 단위 테스트를 수행할 수 있습니다.
11.1.1 unittest
모듈
unittest
는 파이썬 표준 라이브러리로 제공되는 단위 테스트 프레임워크입니다. 이 모듈을 사용하면 테스트 케이스를 정의하고, 실행 결과를 확인할 수 있습니다.
-
기본 구조:
TestCase
클래스: 테스트 케이스를 작성할 때 상속받는 클래스입니다.setUp
메서드: 각 테스트 메서드 실행 전 준비 작업을 수행합니다.tearDown
메서드: 각 테스트 메서드 실행 후 정리 작업을 수행합니다.
-
예제:
import unittest
def add(a, b):
return a + b
class TestMathFunctions(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
self.assertEqual(add(-1, 1), 0)
self.assertEqual(add(-1, -1), -2)
def test_add_floats(self):
self.assertAlmostEqual(add(0.1, 0.2), 0.3, places=1)
if __name__ == "__main__":
unittest.main()
이 예제에서 TestMathFunctions
클래스는 unittest.TestCase
를 상속 받아 add
함수에 대한 여러 테스트를 정의합니다. assertEqual
메서드는 두 값이 동일한지 확인하는 데 사용되며, assertAlmostEqual
은 부동 소수점의 근사값을 비교하는 데 사용됩니다.
11.1.2 pytest
라이브러리
pytest
는 파이썬의 강력한 테스트 프레임워크로, 간결하고 유연한 방식으로 테스트를 작성할 수 있습니다. pytest
는 unittest
와 호환되며, 추가적인 기능과 직관적인 문법을 제공합니다.
-
기본 구조
- 테스트 함수는
test_
로 시작해야 합니다. assert
문을 사용하여 테스트 조건을 정의할 수 있습니다.
- 테스트 함수는
-
예제
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(-1, -1) == -2
def test_add_floats():
assert add(0.1, 0.2) == pytest.approx(0.3)
이 예제에서 pytest.approx
는 부동 소수점 비교를 위한 도우미 함수입니다. pytest
를 사용하면 간결한 문법으로 테스트를 작성할 수 있으며, 다양한 플러그인과 확장을 통해 테스트를 쉽게 관리할 수 있습니다.
- 테스트 실행
pytest
를 설치한 후, 명령줄에서pytest
를 실행하면 자동으로 테스트를 찾아 실행합니다.
$ pytest
11.1.3 테스트 픽스처 (Fixtures)
테스트 픽스처는 테스트 환경을 설정하고 정리하는 데 사용됩니다. unittest
에서는 setUp
과 tearDown
메서드를 사용하고, pytest
에서는 @pytest.fixture
를 통해 더 유연한 픽스처를 정의할 수 있습니다.
- 예제 (
pytest
):
import pytest
@pytest.fixture
def sample_data():
return {"name": "Alice", "age": 30}
def test_sample_data(sample_data):
assert sample_data["name"] == "Alice"
assert sample_data["age"] == 30
이 예제에서 sample_data
픽스처는 테스트에서 사용할 데이터를 설정합니다.
11.2 테스트 주도 개발 (TDD) 기초
**테스트 주도 개발 (Test-Driven Development, TDD)**은 테스트를 먼저 작성하고, 테스트를 통과하는 최소한의 코드를 작성하는 개발 방법론입니다. TDD는 코드의 품질을 높이고, 리팩토링을 쉽게 하며, 버그를 사전에 예방하는 데 도움을 줍니다.
11.2.1 TDD의 기본 흐름
TDD는 다음과 같은 단계로 이루어집니다:
- 테스트 작성: 코드 작성 전에 실패할 테스트를 작성합니다.
- 코드 작성: 테스트를 통과할 수 있는 최소한의 코드를 작성합니다.
- 테스트 실행: 테스트가 성공하는지 확인합니다.
- 리팩토링: 코드와 테스트를 개선하며, 모든 테스트가 통과하는지 확인합니다.
11.2.2 TDD 실습 예제
- 예제:
# 1단계: 실패할 테스트 작성
def test_add():
assert add(2, 3) == 5
# 2단계: 최소한의 코드 작성
def add(a, b):
return a + b
# 3단계: 테스트 실행
# pytest를 사용하여 실행하면, 테스트가 성공합니다.
# 4단계: 리팩토링
# 현재 코드는 간단하여 리팩토링이 필요하지 않지만,
# 더 복잡한 로직일 경우 코드와 테스트를 함께 개선합니다.
이 예제에서는 add
함수의 테스트를 먼저 작성하고, 테스트를 통과하는 코드를 작성하는 단순한 TDD 예제를 보여줍니다.
11.3 디버깅 도구
코드에 버그가 있을 때 이를 추적하고 수정하는 디버깅은 필수적인 과정입니다. 파이썬에서는 다양한 디버깅 도구를 제공하며, 이를 통해 코드의 실행 흐름을 추적하고, 문제를 정확히 파악할 수 있습니다.
11.3.1 pdb
모듈
pdb
는 파이썬의 기본 디버거로, 코드 실행을 한 단계씩 진행하면서 변수 값을 확인하고, 프로그램의 흐름을 제어할 수 있습니다.
- 기본 사용법:
import pdb
def add(a, b):
pdb.set_trace() # 여기서 디버깅 시작
return a + b
result = add(2, 3)
print(result)
이 예제에서 pdb.set_trace()
는 디버깅 세션을 시작하며, 디버거 명령어를 사용해 코드 실행을 제어할 수 있습니다.
- 주요 명령어:
n
(next): 다음 줄로 이동합니다.c
(continue): 디버깅을 종료하고 계속 실행합니다.q
(quit): 디버깅을 중단합니다.p
(print): 변수의 값을 출력합니다.
11.3.2 ipdb
모듈
ipdb
는 pdb
의 확장판으로, IPython을 기반으로 한 더 강력하고 사용하기 쉬운 디버거입니다. ipdb
는 pdb
와 유사하게 동작하지만, 더 나은 출력 형식과 자동 완성 기능을 제공합니다.
- 설치:
$ pip install ipdb
- 사용법:
import ipdb
def add(a, b):
ipdb.set_trace() # ipdb 디버깅 시작
return a + b
result = add(2, 3)
print(result)
11.3.3 pylint
도구
pylint
는 파이썬 코드의 스타일과 에러를 검사하는 도구로, 코드 품질을 향상시키고, 잠재적인 버그를 사전에 발견하는 데 도움을 줍니다.
- 설치:
$ pip install pylint
- 사용법:
$ pylint your_script.py
pylint
는 코드에서 발견된 문제를 리포트하며, 점수를 매겨 코드 품질을 평가합니다.
11.4 프로파일링과 최적화
프로파일링은 코드의 성능을 분석하여, 실행 속도와 메모리 사용량을 파악하고, 성능을 최적화하는 과정입니다. 파이썬에서는 cProfile
과 timeit
을 사용해 코드의 성능을 분석할 수 있습니다.
11.4.1 cProfile
모듈
cProfile
은 파이썬 코드의 실행 시간을 함수 단위로 분석하여, 성능 병목 현상을 파악하는 데 유용한 도구입니다.
- 기본 사용법:
$ python -m cProfile your_script.py
또는 코드 내부에서 직접 사용할 수도 있습니다.
import cProfile
def main():
# 성능 분석할 코드
pass
cProfile.run('main()')
cProfile
은 각 함수의 호출 횟수와 실행 시간을 출력하며, 이를 통해 성능 최적화의 방향을 잡을 수 있습니다.
11.4.2 timeit
모듈
timeit
은 특정 코드의 실행 시간을 측정하는 데 사용됩니다. 간단한 코드 블록의 성능을 비교하거나, 최적화된 코드를 확인할 때 유용합니다.
- 기본 사용법
import timeit
def test():
return sum([i for i in range(100)])
print(timeit.timeit("test()", globals=globals(), number=1000))
이 예제에서 timeit.timeit
은 test
함수의 실행 시간을 1000번 반복하여 측정합니다.
- 커맨드라인 사용법
$ python -m timeit -n 1000 "sum([i for i in range(100)])"
이 명령은 리스트 컴프리헨션을 사용한 sum
함수의 실행 시간을 1000번 반복하여 측정합니다.
11.5 TDD 실습 예제: 고도화된 예제
테스트 주도 개발 (TDD)의 기본 원칙을 이해하고 나면, 이를 실제로 적용하는 더 복잡한 예제를 통해 실습하는 것이 중요합니다. 이번 예제에서는 간단한 은행 계좌 시스템을 구현하는 과정을 통해 TDD를 실습해보겠습니다. 이 과정에서 여러 단계로 나누어 테스트를 작성하고, 이를 기반으로 코드를 작성하며, 필요한 경우 리팩토링을 진행합니다.
예제: 은행 계좌 시스템
시스템 요구사항:
- 계좌는 잔액을 가져야 하며, 기본적으로 0으로 설정된다.
- 계좌에 입금 기능이 있어야 한다.
- 계좌에서 출금 기능이 있어야 한다. 출금 시 잔액이 부족하면 오류가 발생해야 한다.
- 잔액 조회 기능이 있어야 한다.
1단계: 실패할 테스트 작성
먼저, 구현하려는 기능에 대한 테스트를 작성합니다. 이 단계에서는 구현된 코드가 없으므로, 테스트가 실패할 것입니다.
import unittest
class TestBankAccount(unittest.TestCase):
def test_initial_balance(self):
account = BankAccount()
self.assertEqual(account.get_balance(), 0)
def test_deposit(self):
account = BankAccount()
account.deposit(100)
self.assertEqual(account.get_balance(), 100)
def test_withdraw(self):
account = BankAccount()
account.deposit(100)
account.withdraw(40)
self.assertEqual(account.get_balance(), 60)
def test_withdraw_insufficient_funds(self):
account = BankAccount()
account.deposit(50)
with self.assertRaises(ValueError):
account.withdraw(100)
if __name__ == "__main__":
unittest.main()
이 단계에서는 BankAccount
클래스가 아직 구현되지 않았으므로, 이 테스트들은 실패할 것입니다.
2단계: 최소한의 코드 작성
이제 테스트를 통과할 수 있는 최소한의 코드를 작성합니다. 아직 복잡한 기능을 구현하지 말고, 테스트를 통과할 수 있는 가장 단순한 방법으로 코드를 작성합니다.
class BankAccount:
def __init__(self):
self.balance = 0
def get_balance(self):
return self.balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if self.balance < amount:
raise ValueError("잔액이 부족합니다.")
self.balance -= amount
이 코드는 BankAccount
클래스의 기본 기능을 구현합니다. 이제 이 코드를 실행하면 테스트가 통과할 것입니다.
3단계: 테스트 실행
테스트를 실행하여 모든 테스트가 통과하는지 확인합니다. 모든 테스트가 성공하면, 다음 단계로 진행할 수 있습니다.
$ python -m unittest
모든 테스트가 성공적으로 통과해야 합니다.
4단계: 리팩토링
코드가 제대로 동작하고 있음을 확인한 후, 코드와 테스트를 리팩토링하여 코드의 가독성, 재사용성, 유지보수성을 높일 수 있습니다. 리팩토링 후에도 모든 테스트가 통과해야 합니다.
리팩토링의 예:
BankAccount
클래스의 유효성 검사를 추가하고, 예외 메시지를 좀 더 상세하게 작성할 수 있습니다.- 테스트 코드에서 중복되는 부분을
setUp
메서드로 통합하여 간결하게 만들 수 있습니다.
class BankAccount:
def __init__(self, initial_balance=0):
if initial_balance < 0:
raise ValueError("초기 잔액은 음수일 수 없습니다.")
self.balance = initial_balance
def get_balance(self):
return self.balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("입금액은 양수여야 합니다.")
self.balance += amount
def withdraw(self, amount):
if amount <= 0:
raise ValueError("출금액은 양수여야 합니다.")
if self.balance < amount:
raise ValueError("잔액이 부족합니다.")
self.balance -= amount
import unittest
class TestBankAccount(unittest.TestCase):
def setUp(self):
self.account = BankAccount()
def test_initial_balance(self):
self.assertEqual(self.account.get_balance(), 0)
def test_deposit(self):
self.account.deposit(100)
self.assertEqual(self.account.get_balance(), 100)
def test_withdraw(self):
self.account.deposit(100)
self.account.withdraw(40)
self.assertEqual(self.account.get_balance(), 60)
def test_withdraw_insufficient_funds(self):
self.account.deposit(50)
with self.assertRaises(ValueError):
self.account.withdraw(100)
def test_initial_negative_balance(self):
with self.assertRaises(ValueError):
BankAccount(-10)
def test_deposit_negative_amount(self):
with self.assertRaises(ValueError):
self.account.deposit(-50)
def test_withdraw_negative_amount(self):
self.account.deposit(100)
with self.assertRaises(ValueError):
self.account.withdraw(-30)
if __name__ == "__main__":
unittest.main()
고도화된 TDD 예제 요약
- 테스트 작성: 기능을 명확히 정의하고, 실패할 테스트를 먼저 작성합니다.
- 최소한의 코드 작성: 테스트를 통과할 수 있는 가장 간단한 코드를 작성합니다.
- 테스트 실행: 테스트를 실행하여 모든 테스트가 통과하는지 확인합니다.
- 리팩토링: 코드를 개선하면서 테스트가 여전히 통과하는지 확인합니다.
이러한 TDD 프로세스를 반복하면서, 신뢰할 수 있는 고품질의 소프트웨어를 개발할 수 있습니다. TDD는 코드의 품질과 유지보수성을 높이기 위한 강력한 방법론이며, 이를 실제 프로젝트에 적용해보는 것이 중요합니다.