KR_UnitTest - somaz94/python-study GitHub Wiki
unittest๋ ํ์ด์ฌ์ ๊ธฐ๋ณธ ํ
์คํธ ํ๋ ์์ํฌ์ด๋ค. ๊ฐ์ฒด ์งํฅ์ ์ธ ์ ๊ทผ ๋ฐฉ์์ผ๋ก ํ
์คํธ๋ฅผ ๊ตฌ์ฑํ๊ณ ์คํํ ์ ์๊ฒ ํด์ค๋ค.
unittest ํ๋ ์์ํฌ๋ ๋ค์๊ณผ ๊ฐ์ ์ฃผ์ ๊ฐ๋ ์ ๊ธฐ๋ฐ์ผ๋ก ํ๋ค:
- ํ ์คํธ์ผ์ด์ค(TestCase): ๊ฐ๋ณ ํ ์คํธ ๋จ์๋ฅผ ์ ์ํ๋ ํด๋์ค
- ํ ์คํธํฝ์ค์ฒ(Test Fixture): ํ ์คํธ ์คํ ์ ํ์ ํ์ํ ์ค์ ๊ณผ ์ ๋ฆฌ ์์
- ํ ์คํธ์ค์ํธ(Test Suite): ์ฌ๋ฌ ํ ์คํธ ์ผ์ด์ค๋ฅผ ๊ทธ๋ฃนํ
-
ํ
์คํธ๋ฌ๋(Test Runner): ํ
์คํธ๋ฅผ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ณ ํ๋ ์ปดํฌ๋ํธ
import unittest
class Calculator:
def add(self, x, y):
return x + y
def subtract(self, x, y):
return x - y
def multiply(self, x, y):
return x * y
def divide(self, x, y):
if y == 0:
raise ValueError("0์ผ๋ก ๋๋ ์ ์๋ค")
return x / y
class TestCalculator(unittest.TestCase):
def setUp(self):
"""๊ฐ ํ
์คํธ ๋ฉ์๋ ์คํ ์ ์ ํธ์ถ๋๋ค"""
self.calc = Calculator()
def test_add(self):
"""๋ง์
๊ธฐ๋ฅ ํ
์คํธ"""
result = self.calc.add(3, 5)
self.assertEqual(result, 8)
# ๊ฒฝ๊ณ๊ฐ ํ
์คํธ
self.assertEqual(self.calc.add(0, 0), 0)
self.assertEqual(self.calc.add(-1, 1), 0)
def test_subtract(self):
"""๋บ์
๊ธฐ๋ฅ ํ
์คํธ"""
result = self.calc.subtract(10, 5)
self.assertEqual(result, 5)
# ์์ ๊ฒฐ๊ณผ ํ
์คํธ
self.assertEqual(self.calc.subtract(5, 10), -5)
def test_multiply(self):
"""๊ณฑ์
๊ธฐ๋ฅ ํ
์คํธ"""
self.assertEqual(self.calc.multiply(3, 4), 12)
self.assertEqual(self.calc.multiply(0, 5), 0)
self.assertEqual(self.calc.multiply(-2, 3), -6)
def test_divide(self):
"""๋๋์
๊ธฐ๋ฅ ํ
์คํธ"""
self.assertEqual(self.calc.divide(10, 2), 5)
self.assertEqual(self.calc.divide(5, 2), 2.5)
# 0์ผ๋ก ๋๋๊ธฐ ์์ธ ํ
์คํธ
with self.assertRaises(ValueError):
self.calc.divide(10, 0)
def tearDown(self):
"""๊ฐ ํ
์คํธ ๋ฉ์๋ ์คํ ํ์ ํธ์ถ๋๋ค"""
pass
if __name__ == '__main__':
# ํ
์คํธ ์คํ
unittest.main()
# ๋ชจ๋ ์ง์ ์คํ
python test_calculator.py
# unittest ๋ชจ๋ ์ฌ์ฉ
python -m unittest test_calculator.py
# ๋๋ ํ ๋ฆฌ ๋ด ๋ชจ๋ ํ
์คํธ ์คํ
python -m unittest discover
โ
ํน์ง:
- ํ ์คํธ ์ผ์ด์ค๋ฅผ ํด๋์ค๋ก ์ ์ํ์ฌ ๊ตฌ์กฐํ๋ ํ ์คํธ ์์ฑ ๊ฐ๋ฅ
- setUp๊ณผ tearDown ๋ฉ์๋๋ก ํ ์คํธ ์ ํ ํ๊ฒฝ ์ค์ ๋ฐ ์ ๋ฆฌ
- ๋ค์ํ assertion ๋ฉ์๋ ์ ๊ณต(assertEqual, assertTrue, assertRaises ๋ฑ)
- ํ ์คํธ ๊ฒ์ ๋ฐ ์คํ์ ์ํ ๋ช ๋ น์ค ์ธํฐํ์ด์ค ์ ๊ณต
- ํ ์คํธ ๊ฒฐ๊ณผ ๋ณด๊ณ ์ ์์ฑ ๊ธฐ๋ฅ
- ํ ์คํธ ์ค์ํธ๋ฅผ ํตํ ํ ์คํธ ๊ทธ๋ฃนํ ์ง์
- Java์ JUnit์์ ์๊ฐ์ ๋ฐ์ ์ค๊ณ
pytest๋ unittest๋ณด๋ค ๋ ๊ฐ๊ฒฐํ๊ณ ์ ์ฐํ ํ
์คํธ ์์ฑ์ ๊ฐ๋ฅํ๊ฒ ํ๋ ํ๋ ์์ํฌ์ด๋ค. ๊ฐ๋จํ ํจ์๋ก ํ
์คํธ๋ฅผ ์์ฑํ ์ ์์ด ์ง์
์ฅ๋ฒฝ์ด ๋ฎ๋ค.
pytest์ ์ฃผ์ ํน์ง์ ๋ค์๊ณผ ๊ฐ๋ค:
- ๋จ์ํ assert ๋ฌธ์ ์ฌ์ฉํ ๊ฒ์ฆ
- ํํ๋ ฅ์ด ํ๋ถํ ์คํจ ๋ฉ์์ง
- ํ ์คํธ ๋์ค์ปค๋ฒ๋ฆฌ๋ฅผ ํตํ ์๋ ํ ์คํธ ๊ฒ์
- ํฝ์ค์ฒ(fixture)๋ฅผ ํตํ ์์กด์ฑ ์ฃผ์
- ํ๋ฌ๊ทธ์ธ์ ํตํ ํ์ฅ์ฑ
import pytest
# ํ
์คํธํ ํจ์
def is_palindrome(s):
"""๋ฌธ์์ด์ด ์๋ค๋ก ๋์ผํ๊ฒ ์ฝํ๋์ง ํ์ธํ๋ค"""
s = s.lower().replace(" ", "")
return s == s[::-1]
def capitalize_words(text):
"""๋ฌธ์์ด์ ๊ฐ ๋จ์ด ์ฒซ ๊ธ์๋ฅผ ๋๋ฌธ์๋ก ๋ณํํ๋ค"""
return ' '.join(word.capitalize() for word in text.split())
# ๊ธฐ๋ณธ ํ
์คํธ ํจ์
def test_string():
assert 'hello' == 'hello'
def test_list_comparison():
assert [1, 2, 3] == [1, 2, 3]
# ํฐ๋ฆฐ๋๋กฌ ํ
์คํธ
def test_palindrome():
assert is_palindrome("radar") == True
assert is_palindrome("A man a plan a canal Panama") == True
assert is_palindrome("hello") == False
# ์๋ฌ ๋ฐ์ ํ
์คํธ
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0
# ํฝ์ค์ฒ ์ฌ์ฉ ์์
class TestExample:
@pytest.fixture
def sample_data(self):
"""ํ
์คํธ์ ์ฌ์ฉํ ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๋ค"""
return {'name': 'Test', 'value': 42, 'items': [1, 2, 3]}
def test_data(self, sample_data):
assert sample_data['name'] == 'Test'
assert sample_data['value'] == 42
def test_item_count(self, sample_data):
assert len(sample_data['items']) == 3
# ๋งค๊ฐ๋ณ์ํ๋ ํ
์คํธ
@pytest.mark.parametrize('input,expected', [
(2, 4),
(3, 6),
(4, 8),
(0, 0),
(-1, -2)
])
def test_multiply_by_two(input, expected):
assert input * 2 == expected
# ๋งค๊ฐ๋ณ์ํ๋ ํ
์คํธ - ํฐ๋ฆฐ๋๋กฌ
@pytest.mark.parametrize('string,is_valid', [
("racecar", True),
("level", True),
("hello", False),
("A Santa at NASA", True),
("", True)
])
def test_palindrome_parameterized(string, is_valid):
assert is_palindrome(string) == is_valid
# ๋งค๊ฐ๋ณ์ํ๋ ํ
์คํธ - ๋๋ฌธ์ ๋ณํ
@pytest.mark.parametrize('input_str,expected', [
("hello world", "Hello World"),
("python testing", "Python Testing"),
("", "")
])
def test_capitalize_words(input_str, expected):
assert capitalize_words(input_str) == expected
# ๊ธฐ๋ณธ ์คํ
pytest
# ํน์ ํ์ผ ์คํ
pytest test_sample.py
# ์์ธ ์ถ๋ ฅ
pytest -v
# ์คํจํ ํ
์คํธ๋ง ์คํ
pytest --last-failed
# ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง ํ์ธ
pytest --cov=mymodule
โ
ํน์ง:
- ๊ฐ๋จํ assert ๋ฌธ๋ง์ผ๋ก ํ ์คํธ ์์ฑ ๊ฐ๋ฅ
- ์คํจ ์ ์์ธํ ์ฐจ์ด์ ๋ณด๊ณ ๋ก ๋๋ฒ๊น ์ฉ์ด
- fixture๋ฅผ ํตํ ํ ์คํธ ์ค์ ๋ฐ ์์กด์ฑ ๊ด๋ฆฌ
- parametrize ๋ฐ์ฝ๋ ์ดํฐ๋ก ๋ค์ํ ์ ๋ ฅ์ ๋ํ ํ ์คํธ ๊ฐ๊ฒฐํ๊ฒ ์์ฑ
- ํ๋ฌ๊ทธ์ธ ์ํ๊ณ๋ฅผ ํตํ ๊ธฐ๋ฅ ํ์ฅ(coverage, benchmark ๋ฑ)
- unittest์์ ํธํ์ฑ ์ง์
- ๋ง์ปค๋ฅผ ํตํ ํ ์คํธ ๋ถ๋ฅ ๋ฐ ์ ํ์ ์คํ
- ๋ณ๋ ฌ ํ ์คํธ ์คํ ์ง์์ผ๋ก ์คํ ์๊ฐ ๋จ์ถ
๋ชจ์ ๊ฐ์ฒด(Mock)๋ ์ค์ ๊ฐ์ฒด์ ๋์์ ์๋ฎฌ๋ ์ด์
ํ์ฌ ํ
์คํธ๋ฅผ ๋
๋ฆฝ์ ์ผ๋ก ์ํํ ์ ์๊ฒ ํด์ฃผ๋ ๋๊ตฌ์ด๋ค. ์ธ๋ถ ์์กด์ฑ์ด ์๋ ์ฝ๋๋ฅผ ๊ฒฉ๋ฆฌํ์ฌ ํ
์คํธํ๋ ๋ฐ ์ ์ฉํ๋ค.
๋ชจ์ ๊ฐ์ฒด์ ์ฃผ์ ์ฉ๋๋ ๋ค์๊ณผ ๊ฐ๋ค:
- ์ธ๋ถ ์๋น์ค๋ API ํธ์ถ ๋์ฒด
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์์ด ํ ์คํธ ์ํ
- ๋ณต์กํ ๊ฐ์ฒด์ ๋์ ์๋ฎฌ๋ ์ด์
- ํน์ ์์ธ ์ํฉ ํ ์คํธ
- ํจ์ ํธ์ถ ์ฌ๋ถ ๋ฐ ์ธ์ ๊ฒ์ฆ
from unittest.mock import Mock, patch, MagicMock, call
# ํ
์คํธํ ํด๋์ค
class UserService:
def __init__(self, db):
self.db = db
def get_user(self, user_id):
return self.db.find_user(user_id)
def create_user(self, user_data):
if not user_data.get('name'):
raise ValueError("์ฌ์ฉ์ ์ด๋ฆ์ ํ์์ด๋ค")
return self.db.insert_user(user_data)
def update_user_email(self, user_id, new_email):
user = self.db.find_user(user_id)
if not user:
return False
user['email'] = new_email
return self.db.update_user(user_id, user)
# Mock ๊ฐ์ฒด ๊ธฐ๋ณธ ์ฌ์ฉ
def test_get_user():
# Mock ๊ฐ์ฒด ์์ฑ
mock_db = Mock()
# ๋ฐํ๊ฐ ์ค์
mock_db.find_user.return_value = {
'id': 1,
'name': 'Test User',
'email': '[email protected]'
}
service = UserService(mock_db)
user = service.get_user(1)
# ๊ฒฐ๊ณผ ๊ฒ์ฆ
assert user['name'] == 'Test User'
assert user['email'] == '[email protected]'
# Mock ํธ์ถ ๊ฒ์ฆ
mock_db.find_user.assert_called_once_with(1)
# ์์ธ ์ํฉ ํ
์คํธ
def test_create_user_with_invalid_data():
mock_db = Mock()
service = UserService(mock_db)
# ์์ธ ๋ฐ์ ํ์ธ
try:
service.create_user({})
assert False, "์์ธ๊ฐ ๋ฐ์ํด์ผ ํ๋ค"
except ValueError as e:
assert str(e) == "์ฌ์ฉ์ ์ด๋ฆ์ ํ์์ด๋ค"
# DB ๋ฉ์๋๊ฐ ํธ์ถ๋์ง ์์๋์ง ํ์ธ
mock_db.insert_user.assert_not_called()
# ์ฌ๋ฌ ํธ์ถ์ ๋ํ ๋ค๋ฅธ ๋ฐํ๊ฐ ์ค์
def test_update_user_email():
mock_db = Mock()
# ์ฒซ ๋ฒ์งธ ํธ์ถ์ ์ฌ์ฉ์ ๋ฐ์ดํฐ ๋ฐํ, ๋ ๋ฒ์งธ ํธ์ถ์ ์
๋ฐ์ดํธ ์ฑ๊ณต ๋ฐํ
mock_db.find_user.return_value = {'id': 1, 'name': 'Old Name', 'email': '[email protected]'}
mock_db.update_user.return_value = True
service = UserService(mock_db)
result = service.update_user_email(1, '[email protected]')
assert result == True
# ์
๋ฐ์ดํธ ํธ์ถ ์ ์ ๋ฌ๋ ์ด๋ฉ์ผ ํ์ธ
mock_db.update_user.assert_called_once()
args, kwargs = mock_db.update_user.call_args
assert args[1]['email'] == '[email protected]'
# ๋ชจ๋ ๋๋ ํจ์ ํจ์นํ๊ธฐ
import requests
def get_user_data(user_id):
"""์ธ๋ถ API์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์จ๋ค"""
response = requests.get(f'https://api.example.com/users/{user_id}')
if response.status_code == 200:
return response.json()
return None
@patch('requests.get')
def test_api_call(mock_get):
# Mock ์๋ต ์ค์
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {'id': 123, 'name': 'API User'}
mock_get.return_value = mock_response
# ํจ์ ํธ์ถ
result = get_user_data(123)
# ๊ฒฐ๊ณผ ๊ฒ์ฆ
assert result['name'] == 'API User'
mock_get.assert_called_once_with('https://api.example.com/users/123')
# ํด๋์ค ๋ฉ์๋ ํจ์นํ๊ธฐ
class ExternalService:
@classmethod
def get_data(cls):
# ์ค์ ๋ก๋ ์ธ๋ถ ์๋น์ค์ ์ ๊ทผ
return "์ค์ ๋ฐ์ดํฐ"
def process_data():
data = ExternalService.get_data()
return f"์ฒ๋ฆฌ๋ ๋ฐ์ดํฐ: {data}"
@patch.object(ExternalService, 'get_data')
def test_process_data(mock_get_data):
mock_get_data.return_value = "ํ
์คํธ ๋ฐ์ดํฐ"
result = process_data()
assert result == "์ฒ๋ฆฌ๋ ๋ฐ์ดํฐ: ํ
์คํธ ๋ฐ์ดํฐ"
mock_get_data.assert_called_once()
# ์ฌ๋ฌ ํธ์ถ์ ๋ํ ์ฐ์์ ์ธ ๋ฐํ๊ฐ ์ค์
def test_multiple_calls():
mock = Mock()
mock.side_effect = [1, 2, 3, Exception("์๋ฌ ๋ฐ์")]
assert mock() == 1
assert mock() == 2
assert mock() == 3
try:
mock()
assert False, "์์ธ๊ฐ ๋ฐ์ํด์ผ ํ๋ค"
except Exception as e:
assert str(e) == "์๋ฌ ๋ฐ์"
# ๋ณต์กํ ํธ์ถ ํจํด ๊ฒ์ฆ
def test_complex_call_pattern():
mock = Mock()
mock(1)
mock(2)
mock(3, key='value')
# ํธ์ถ ์์์ ๋งค๊ฐ๋ณ์ ๊ฒ์ฆ
expected_calls = [call(1), call(2), call(3, key='value')]
mock.assert_has_calls(expected_calls, any_order=False)
-
MagicMock:
__getitem__
,__len__
๋ฑ ํน์ ๋ฉ์๋๋ฅผ ์๋์ผ๋ก ๊ตฌํํ Mock ํ์ฅ
-
patch ๋ฐ์ฝ๋ ์ดํฐ/์ปจํ
์คํธ ๊ด๋ฆฌ์: ์ง์ ๋ ๊ฐ์ฒด๋ฅผ ์ผ์์ ์ผ๋ก ๋ชจ์ ๊ฐ์ฒด๋ก ๋์ฒด
-
side_effect: ์ฌ๋ฌ ํธ์ถ์ ๋ํ ๋ค์ํ ๊ฒฐ๊ณผ๋ ๋์ ์ง์
-
assert_called_with: ํน์ ์ธ์๋ก ํธ์ถ๋์๋์ง ํ์ธ
-
assert_has_calls: ์ฌ๋ฌ ํธ์ถ ํจํด ๊ฒ์ฆ
-
return_value: ๋ชจ์ ๊ฐ์ฒด๊ฐ ๋ฐํํ ๊ฐ ์ค์
-
autospec: ์๋ณธ ๊ฐ์ฒด์ API๋ฅผ ๊ทธ๋๋ก ์ ์งํ๋๋ก ๋ชจ์ ๊ฐ์ฒด ์์ฑ
โ
ํน์ง:
- ์ธ๋ถ ์์กด์ฑ์ ์ ๊ฑฐํ์ฌ ๋จ์ ํ ์คํธ์ ์ ๋ขฐ์ฑ ํฅ์
- ํ ์คํธ ํ๊ฒฝ์์ ์ฌํํ๊ธฐ ์ด๋ ค์ด ์ํฉ ์๋ฎฌ๋ ์ด์ ๊ฐ๋ฅ
- ํจ์๋ ๋ฉ์๋์ ํธ์ถ ์ฌ๋ถ, ํ์, ์ธ์ ๋ฑ์ ์์ธํ ๊ฒ์ฆ
- ๋ค์ํ ์๋๋ฆฌ์ค๋ฅผ ํ ์คํธํ๊ธฐ ์ํ ์ ์ฐํ ์๋ต ์ค์
- ์ค์ ๊ตฌํ์ด ์๋ฃ๋์ง ์์ ์ปดํฌ๋ํธ๋ ํ ์คํธ ๊ฐ๋ฅ
- ํ ์คํธ ์คํ ์๋ ํฅ์ (์ค์ API๋ DB ์ฐ๊ฒฐ ์์ด ํ ์คํธ)
- ํน์ํ ์ํฉ(๋คํธ์ํฌ ์ค๋ฅ, ํ์์์ ๋ฑ)์ ๋ํ ํ ์คํธ ์ฉ์ด
ํ
์คํธ ์คํ๊ณผ ๋๋ฒ๊น
์ ํจ์จ์ ์ธ ํ
์คํธ ํ๋ก์ธ์ค๋ฅผ ์ํ ํต์ฌ ์์์ด๋ค. ํ
์คํธ ์ ํ ์ํ ์ค์ ๊ณผ ์ ๋ฆฌ, ํ
์คํธ ๊ฒฐ๊ณผ ๋ถ์์ ํตํด ์ฝ๋์ ์ ๋ขฐ์ฑ์ ๋์ผ ์ ์๋ค.
import unittest
import tempfile
import os
import logging
# ๋ก๊น
์ค์
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class FileProcessor:
def __init__(self, filename):
self.filename = filename
def write_data(self, data):
with open(self.filename, 'w') as f:
f.write(data)
return len(data)
def read_data(self):
try:
with open(self.filename, 'r') as f:
return f.read()
except FileNotFoundError:
return None
class TestWithSetup(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""์ ์ฒด ํ
์คํธ ์์ ์ 1ํ ์คํ๋๋ค"""
logger.info("ํ
์คํธ ํด๋์ค ์์")
# ์์ ๋๋ ํ ๋ฆฌ ์์ฑ
cls.test_dir = tempfile.mkdtemp()
def setUp(self):
"""๊ฐ ํ
์คํธ ๋ฉ์๋ ์์ ์ ์คํ๋๋ค"""
logger.info(f"ํ
์คํธ ์์: {self._testMethodName}")
self.test_data = [1, 2, 3]
# ํ
์คํธ ํ์ผ ๊ฒฝ๋ก ์์ฑ
self.test_file = os.path.join(self.__class__.test_dir, "test_file.txt")
self.processor = FileProcessor(self.test_file)
def test_list_operations(self):
"""๋ฆฌ์คํธ ์ฐ์ฐ ํ
์คํธ"""
self.test_data.append(4)
self.assertEqual(len(self.test_data), 4)
self.assertIn(4, self.test_data)
def test_file_operations(self):
"""ํ์ผ ์์
ํ
์คํธ"""
test_content = "ํ
์คํธ ๋ด์ฉ"
written_bytes = self.processor.write_data(test_content)
self.assertEqual(written_bytes, len(test_content))
content = self.processor.read_data()
self.assertEqual(content, test_content)
def tearDown(self):
"""๊ฐ ํ
์คํธ ๋ฉ์๋ ์ข
๋ฃ ํ ์คํ๋๋ค"""
logger.info(f"ํ
์คํธ ์ข
๋ฃ: {self._testMethodName}")
# ํ
์คํธ ํ์ผ์ด ์์ผ๋ฉด ์ญ์
if os.path.exists(self.test_file):
os.remove(self.test_file)
@classmethod
def tearDownClass(cls):
"""์ ์ฒด ํ
์คํธ ์ข
๋ฃ ํ 1ํ ์คํ๋๋ค"""
logger.info("ํ
์คํธ ํด๋์ค ์ข
๋ฃ")
# ์์ ๋๋ ํ ๋ฆฌ ์ญ์
os.rmdir(cls.test_dir)
# ๋ชจ๋ ํ
์คํธ ์คํ
python -m unittest discover
# ํน์ ๋ชจ๋ ์คํ
python -m unittest test_module.py
# ํ
์คํธ ์์ธ ์ถ๋ ฅ
python -m unittest -v test_module.py
โ
ํน์ง:
- ํ ์คํธ ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ(setUp, tearDown, setUpClass, tearDownClass)
- ๊ฐ๋ณ ํ ์คํธ์ ํ ์คํธ ํด๋์ค ์์ค์ ์ค์ ๋ฐ ์ ๋ฆฌ
- ํ ์คํธ ์คํ ์ค ์ํ ๋ก๊น
- ์์ ํ์ผ ๋ฐ ๋๋ ํ ๋ฆฌ ํ์ฉ์ผ๋ก ํ ์คํธ ๊ฒฉ๋ฆฌ์ฑ ๋ณด์ฅ
- ๋ค์ํ ์คํ ์ต์ (verbosity, failfast, catch ๋ฑ)
- ๋๋ฒ๊น ๋๊ตฌ์์ ํตํฉ์ ํตํ ๋ฌธ์ ํด๊ฒฐ ์ง์
- ํ ์คํธ ์คํจ ์์ธ ํ์ ์ ์ํ ๋ค์ํ ๋๊ตฌ ์ ๊ณต
ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ ์ฝ๋๊ฐ ํ
์คํธ๋ก ์ผ๋ง๋ ์ ๊ฒ์ฆ๋์๋์ง ์ธก์ ํ๋ ์งํ์ด๋ค. ๋์ ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ ๋ฒ๊ทธ ๋ฐ์ ๊ฐ๋ฅ์ฑ์ ์ค์ด๊ณ ์ฝ๋ ํ์ง์ ํฅ์์ํจ๋ค.
ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง์ ์ฃผ์ ์ ํ์ ๋ค์๊ณผ ๊ฐ๋ค:
- ๊ตฌ๋ฌธ ์ปค๋ฒ๋ฆฌ์ง: ๊ฐ ์ฝ๋ ๋ผ์ธ์ด ์คํ๋๋์ง ์ธก์
- ๋ถ๊ธฐ ์ปค๋ฒ๋ฆฌ์ง: ๋ชจ๋ ์กฐ๊ฑด๋ถ ๋ถ๊ธฐ๊ฐ ์คํ๋๋์ง ์ธก์
- ๊ฒฝ๋ก ์ปค๋ฒ๋ฆฌ์ง: ๊ฐ๋ฅํ ๋ชจ๋ ์คํ ๊ฒฝ๋ก๊ฐ ํ ์คํธ๋๋์ง ์ธก์
-
ํจ์ ์ปค๋ฒ๋ฆฌ์ง: ๊ฐ ํจ์๋ ๋ฉ์๋๊ฐ ํธ์ถ๋๋์ง ์ธก์
# ํ
์คํธ ๋์ ์ฝ๋
def calculate_grade(score):
"""์ ์์ ๋ฐ๋ผ ํ์ ์ ๊ณ์ฐํ๋ค"""
if score < 0 or score > 100:
raise ValueError("์ ์๋ 0~100 ์ฌ์ด์ฌ์ผ ํ๋ค")
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
# ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ์ ์ํ ํ
์คํธ
import unittest
class TestGradeCalculation(unittest.TestCase):
def test_invalid_scores(self):
"""์ ํจํ์ง ์์ ์ ์ ํ
์คํธ"""
with self.assertRaises(ValueError):
calculate_grade(-10)
with self.assertRaises(ValueError):
calculate_grade(110)
def test_grade_a(self):
"""Aํ์ ํ
์คํธ"""
self.assertEqual(calculate_grade(95), 'A')
self.assertEqual(calculate_grade(90), 'A')
def test_grade_b(self):
"""Bํ์ ํ
์คํธ"""
self.assertEqual(calculate_grade(85), 'B')
self.assertEqual(calculate_grade(80), 'B')
def test_grade_c(self):
"""Cํ์ ํ
์คํธ"""
self.assertEqual(calculate_grade(75), 'C')
self.assertEqual(calculate_grade(70), 'C')
def test_grade_d(self):
"""Dํ์ ํ
์คํธ"""
self.assertEqual(calculate_grade(65), 'D')
self.assertEqual(calculate_grade(60), 'D')
def test_grade_f(self):
"""Fํ์ ํ
์คํธ"""
self.assertEqual(calculate_grade(55), 'F')
self.assertEqual(calculate_grade(0), 'F')
# ๊ฒฝ๊ณ๊ฐ ํ
์คํธ
def test_boundary_values(self):
"""๊ฒฝ๊ณ๊ฐ ํ
์คํธ"""
self.assertEqual(calculate_grade(89), 'B') # A์ ํํ ๊ฒฝ๊ณ ๋ฐ๋ก ์๋
self.assertEqual(calculate_grade(90), 'A') # A์ ํํ ๊ฒฝ๊ณ
self.assertEqual(calculate_grade(79), 'C') # B์ ํํ ๊ฒฝ๊ณ ๋ฐ๋ก ์๋
self.assertEqual(calculate_grade(80), 'B') # B์ ํํ ๊ฒฝ๊ณ
Python์์๋ coverage
๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ํ
์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ธก์ ํ ์ ์๋ค.
# coverage ์ค์น
pip install coverage
# ํ
์คํธ ์คํํ๋ฉฐ ์ปค๋ฒ๋ฆฌ์ง ์ธก์
coverage run -m unittest discover
# ์ปค๋ฒ๋ฆฌ์ง ๋ณด๊ณ ์ ์์ฑ
coverage report -m
# HTML ํ์์ ์์ธ ๋ณด๊ณ ์ ์์ฑ
coverage html
import unittest
from parameterized import parameterized # pip install parameterized
class TestParameterized(unittest.TestCase):
@parameterized.expand([
(0, 'F'),
(55, 'F'),
(60, 'D'),
(70, 'C'),
(80, 'B'),
(90, 'A'),
(100, 'A'),
])
def test_grades(self, score, expected_grade):
"""๋ค์ํ ์ ์์ ๋ํ ํ์ ํ
์คํธ"""
self.assertEqual(calculate_grade(score), expected_grade)
์์ฑ ๊ธฐ๋ฐ ํ
์คํธ๋ ์
๋ ฅ ๋ฐ์ดํฐ์ ๋ํ ํน์ ์์ฑ์ด๋ ๊ท์น์ด ์ ์ง๋๋์ง ๊ฒ์ฆํ๋ค.
# pip install hypothesis
from hypothesis import given, strategies as st
import unittest
class TestProperties(unittest.TestCase):
@given(st.integers(min_value=90, max_value=100))
def test_a_grade_property(self, score):
"""90~100 ์ฌ์ด ๋ชจ๋ ์ ์๋ Aํ์ ์ด๋ค"""
self.assertEqual(calculate_grade(score), 'A')
@given(st.integers(min_value=0, max_value=59))
def test_f_grade_property(self, score):
"""0~59 ์ฌ์ด ๋ชจ๋ ์ ์๋ Fํ์ ์ด๋ค"""
self.assertEqual(calculate_grade(score), 'F')
@given(st.integers().filter(lambda x: x < 0 or x > 100))
def test_invalid_score_property(self, score):
"""๋ฒ์ ๋ฐ์ ๋ชจ๋ ์ ์๋ ValueError๋ฅผ ๋ฐ์์ํจ๋ค"""
with self.assertRaises(ValueError):
calculate_grade(score)
โ
ํน์ง:
- ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ๋๊ตฌ๋ฅผ ํตํ ํ ์คํธ ๋ฒ์ ์๊ฐํ
- ๋งค๊ฐ๋ณ์ํ๋ ํ ์คํธ๋ก ๋ค์ํ ์ ๋ ฅ ๊ฒ์ฆ
- ์์ฑ ๊ธฐ๋ฐ ํ ์คํธ๋ก ๊ท์น ๋ฐ ๋ถ๋ณ ์กฐ๊ฑด ๊ฒ์ฆ
- ๊ฒฝ๊ณ๊ฐ ํ ์คํธ๋ฅผ ํตํ ์ทจ์ฝ ์ง์ ์ง์ค ๊ฒ์ฆ
- ํ ์คํธ ๋ณด๊ณ ์ ์๋ํ๋ก ์ง์์ ์ธ ํ์ง ๋ชจ๋ํฐ๋ง
- ๋ฏธ๊ฒ์ฆ ์ฝ๋ ์์ญ ์๋ณ์ ํตํ ํ ์คํธ ๊ฐ์
- ํ ์คํธ ๋ฐ์ดํฐ ์์ฑ ์๋ํ๋ฅผ ํตํ ํจ์จ์ฑ ํฅ์
ํตํฉ ํ
์คํธ๋ ์ฌ๋ฌ ์ปดํฌ๋ํธ๊ฐ ํจ๊ป ๋์ํ ๋ ์ฌ๋ฐ๋ฅด๊ฒ ์๋ํ๋์ง ๊ฒ์ฆํ๋ ํ
์คํธ์ด๋ค. ๋จ์ ํ
์คํธ์ ๋ฌ๋ฆฌ ์ค์ ์์กด์ฑ์ ์ฌ์ฉํ์ฌ ์์คํ
๊ฐ ์ํธ์์ฉ์ ํ
์คํธํ๋ค.
ํตํฉ ํ ์คํธ์ ๋ชฉ์ ์ ๋ค์๊ณผ ๊ฐ๋ค:
- ์ปดํฌ๋ํธ ๊ฐ ์ธํฐํ์ด์ค ํธํ์ฑ ๊ฒ์ฆ
- ๋ฐ์ดํฐ ํ๋ฆ ์ ํ์ฑ ํ์ธ
- ์ธ๋ถ ์์คํ ๊ณผ์ ์ฌ๋ฐ๋ฅธ ํตํฉ ํ์ธ
- ์ค์ ํ๊ฒฝ๊ณผ ์ ์ฌํ ์กฐ๊ฑด์์์ ๋์ ๊ฒ์ฆ
import unittest
import sqlite3
import os
import tempfile
# ํ
์คํธํ ํด๋์ค๋ค
class Database:
def __init__(self, db_file):
self.db_file = db_file
self.connection = None
def connect(self):
self.connection = sqlite3.connect(self.db_file)
self.connection.row_factory = sqlite3.Row
def close(self):
if self.connection:
self.connection.close()
def execute(self, query, params=()):
cursor = self.connection.cursor()
cursor.execute(query, params)
self.connection.commit()
return cursor
class UserRepository:
def __init__(self, database):
self.db = database
def create_table(self):
self.db.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL
)
''')
def add_user(self, username, email):
cursor = self.db.execute(
"INSERT INTO users (username, email) VALUES (?, ?)",
(username, email)
)
return cursor.lastrowid
def get_user(self, user_id):
cursor = self.db.execute("SELECT * FROM users WHERE id = ?", (user_id,))
return cursor.fetchone()
def get_by_username(self, username):
cursor = self.db.execute("SELECT * FROM users WHERE username = ?", (username,))
return cursor.fetchone()
class UserService:
def __init__(self, user_repository):
self.repository = user_repository
def register_user(self, username, email):
# ์ฌ์ฉ์๋ช
์ค๋ณต ํ์ธ
existing_user = self.repository.get_by_username(username)
if existing_user:
raise ValueError(f"์ฌ์ฉ์๋ช
'{username}'์ด ์ด๋ฏธ ์กด์ฌํ๋ค")
# ์ด๋ฉ์ผ ํ์ ๊ฒ์ฆ
if '@' not in email:
raise ValueError("์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋ค")
# ๋ฑ๋ก
return self.repository.add_user(username, email)
def get_user_details(self, user_id):
user = self.repository.get_user(user_id)
if not user:
return None
# ์ฌ์ฉ์ ์ ๋ณด ๋ฐํ
return dict(user)
# ํตํฉ ํ
์คํธ
class TestUserSystem(unittest.TestCase):
def setUp(self):
# ํ
์คํธ์ฉ ์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ผ ์์ฑ
self.db_file = tempfile.NamedTemporaryFile(delete=False).name
# ์ปดํฌ๋ํธ ์ด๊ธฐํ
self.database = Database(self.db_file)
self.database.connect()
self.user_repo = UserRepository(self.database)
self.user_repo.create_table()
self.user_service = UserService(self.user_repo)
def test_user_registration_workflow(self):
"""์ฌ์ฉ์ ๋ฑ๋ก ์ํฌํ๋ก์ฐ ํตํฉ ํ
์คํธ"""
# ์ ์ฌ์ฉ์ ๋ฑ๋ก
user_id = self.user_service.register_user("testuser", "[email protected]")
self.assertIsNotNone(user_id)
# ๋ฑ๋ก๋ ์ฌ์ฉ์ ์ ๋ณด ์กฐํ
user = self.user_service.get_user_details(user_id)
self.assertEqual(user["username"], "testuser")
self.assertEqual(user["email"], "[email protected]")
# ์ค๋ณต ์ฌ์ฉ์๋ช
๋ฑ๋ก ์๋
with self.assertRaises(ValueError):
self.user_service.register_user("testuser", "[email protected]")
def test_invalid_email_format(self):
"""์๋ชป๋ ์ด๋ฉ์ผ ํ์ ํ
์คํธ"""
with self.assertRaises(ValueError):
self.user_service.register_user("newuser", "invalid-email")
def tearDown(self):
# ์ฐ๊ฒฐ ์ข
๋ฃ
self.database.close()
# ํ
์คํธ ํ์ผ ์ญ์
os.unlink(self.db_file)
REST API์์ ํตํฉ์ ํ
์คํธํ๋ ์์์ด๋ค.
import unittest
import requests
import json
from unittest.mock import patch
# ํ
์คํธํ API ํด๋ผ์ด์ธํธ
class WeatherApiClient:
def __init__(self, api_key, base_url="https://api.weather.example"):
self.api_key = api_key
self.base_url = base_url
def get_current_weather(self, city):
url = f"{self.base_url}/current"
params = {
"city": city,
"apikey": self.api_key
}
response = requests.get(url, params=params)
if response.status_code == 200:
return response.json()
elif response.status_code == 404:
return {"error": "๋์๋ฅผ ์ฐพ์ ์ ์๋ค"}
else:
response.raise_for_status()
# ๋ ์จ ์ ๋ณด๋ฅผ ์ฌ์ฉํ๋ ์๋น์ค
class WeatherService:
def __init__(self, weather_client):
self.client = weather_client
def get_temperature(self, city):
weather_data = self.client.get_current_weather(city)
if "error" in weather_data:
return None
return weather_data.get("temperature")
def should_wear_jacket(self, city):
temp = self.get_temperature(city)
if temp is None:
return None
return temp < 15.0
# ๋ชจ์ ์๋ฒ ์๋ต์ ์ฌ์ฉํ ํตํฉ ํ
์คํธ
class TestWeatherService(unittest.TestCase):
def setUp(self):
self.api_key = "test_api_key"
self.client = WeatherApiClient(self.api_key)
self.service = WeatherService(self.client)
@patch('requests.get')
def test_get_temperature(self, mock_get):
# ๋ชจ์ ์๋ต ์ค์
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = json.dumps({"temperature": 20.5, "humidity": 65}).encode()
mock_get.return_value = mock_response
# ์๋น์ค ๋ฉ์๋ ํธ์ถ
temp = self.service.get_temperature("Seoul")
# ์์ ๊ฒฐ๊ณผ ๊ฒ์ฆ
self.assertEqual(temp, 20.5)
mock_get.assert_called_once()
# ํธ์ถ URL๊ณผ ๋งค๊ฐ๋ณ์ ๊ฒ์ฆ
args, kwargs = mock_get.call_args
self.assertEqual(kwargs['params']['city'], "Seoul")
self.assertEqual(kwargs['params']['apikey'], self.api_key)
@patch('requests.get')
def test_should_wear_jacket_cold(self, mock_get):
# ์ถ์ด ๋ ์จ ์๋ต ๋ชจ์
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = json.dumps({"temperature": 10.0}).encode()
mock_get.return_value = mock_response
# ์์ผ ํ์ ์ฌ๋ถ ์ฒดํฌ
result = self.service.should_wear_jacket("Moscow")
# ์ถ์ด ๋ ์จ์๋ ์์ผ์ด ํ์ํ๋ค
self.assertTrue(result)
@patch('requests.get')
def test_should_wear_jacket_warm(self, mock_get):
# ๋ฐ๋ปํ ๋ ์จ ์๋ต ๋ชจ์
mock_response = requests.Response()
mock_response.status_code = 200
mock_response._content = json.dumps({"temperature": 25.0}).encode()
mock_get.return_value = mock_response
# ์์ผ ํ์ ์ฌ๋ถ ์ฒดํฌ
result = self.service.should_wear_jacket("Hawaii")
# ๋ฐ๋ปํ ๋ ์จ์๋ ์์ผ์ด ํ์ํ์ง ์๋ค
self.assertFalse(result)
โ
ํน์ง:
- ์ปดํฌ๋ํธ ๊ฐ ์ํธ์์ฉ ๋ฐ ํตํฉ ๊ฒ์ฆ
- ์ค์ ๋๋ ๋ชจ์ ์ธ๋ถ ์์คํ ์ ์ฌ์ฉํ ํ ์คํธ
- ์ ์ฒด ์ํฌํ๋ก์ฐ ๋ฐ ๋น์ฆ๋์ค ํ๋ก์ธ์ค ๊ฒ์ฆ
- ๋ฐ์ดํฐ ๋ณํ ๋ฐ ์ ์ก ์ ํ์ฑ ํ์ธ
- ์๋ฌ ์ฒ๋ฆฌ ๋ฐ ์์ธ ์ํฉ ํ ์คํธ
- ์ค์ ํ๊ฒฝ๊ณผ ์ ์ฌํ ์๋๋ฆฌ์ค ๊ธฐ๋ฐ ํ ์คํธ
- ์ฑ๋ฅ, ๋ณด์, ์ ๋ขฐ์ฑ ๋ฑ์ ๋น๊ธฐ๋ฅ์ ์๊ตฌ์ฌํญ ๊ฒ์ฆ
โ
๋ชจ๋ฒ ์ฌ๋ก:
-
ํ ์คํธ ๊ฒฉ๋ฆฌ ์ ์ง: ๊ฐ ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ผ๋ก ์คํ๋ ์ ์๋๋ก ์ค๊ณํ์ฌ ํ ์คํธ ๊ฐ ์์กด์ฑ์ ๋ฐฉ์งํ์.
-
๋ช ํํ ํ ์คํธ ์ด๋ฆ ์ฌ์ฉ: ํ ์คํธ ์ด๋ฆ์ ๋ฌด์์ ํ ์คํธํ๋์ง ๋ช ํํ ํํํ์ฌ ํ ์คํธ ์คํจ ์ ๋น ๋ฅด๊ฒ ํ์ ํ ์ ์๊ฒ ํ์.
-
ํ๋์ ํ ์คํธ๋ ํ๋์ ๋์๋ง ๊ฒ์ฆ: ๊ฐ ํ ์คํธ๋ ๋จ์ผ ๊ธฐ๋ฅ์ด๋ ๋์์ ์ง์คํ์ฌ ๋ณต์ก์ฑ์ ์ค์ด๊ณ ์ ์ง๋ณด์์ฑ์ ๋์ด์.
-
ํ ์คํธ ํฝ์ค์ฒ ํ์ฉ: ๊ณตํต ์ค์ ์ฝ๋๋ฅผ ํฝ์ค์ฒ๋ก ๋ถ๋ฆฌํ์ฌ ์ฝ๋ ์ค๋ณต์ ์ค์ด๊ณ ์ฌ์ฌ์ฉ์ฑ์ ๋์ด์.
-
์ ์ ํ assertion ์ฌ์ฉ: ์ํฉ์ ๋ง๋ ๊ตฌ์ฒด์ ์ธ assertion์ ์ ํํ์ฌ ์คํจ ๋ฉ์์ง์ ๋ช ํ์ฑ์ ๋์ด์.
-
ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง ๊ด๋ฆฌ: ์ค์ํ ๋น์ฆ๋์ค ๋ก์ง์ ๋์ ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ ์งํ์ฌ ๋ฒ๊ทธ ๋ฐ์ ๊ฐ๋ฅ์ฑ์ ์ค์ด์.
-
CI/CD ํตํฉ: ์ง์์ ํตํฉ ํ๊ฒฝ์์ ํ ์คํธ๋ฅผ ์๋์ผ๋ก ์คํํ์ฌ ๋น ๋ฅธ ํผ๋๋ฐฑ์ ๋ฐ์ ์ ์๊ฒ ํ์.
-
ํ ์คํธ ์๋ ์ต์ ํ: ๋๋ฆฐ ํ ์คํธ๋ ๊ฐ๋ฐ ์์ฐ์ฑ์ ์ ํ์ํค๋ฏ๋ก, ๋น ๋ฅด๊ฒ ์คํ๋๋๋ก ์ต์ ํํ์.
-
ํ ์คํธ ๋ฐ์ดํฐ ๊ด๋ฆฌ: ํ ์คํธ์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ช ํํ๊ฒ ์ค์ ํ๊ณ ํ ์คํธ ํ ์ ๋ฆฌํ์ฌ ํ๊ฒฝ ์ค์ผ์ ๋ฐฉ์งํ์.
-
์ ๊ธฐ์ ์ธ ๋ฆฌํฉํ ๋ง: ํ ์คํธ ์ฝ๋๋ ์ ๊ธฐ์ ์ผ๋ก ๋ฆฌํฉํ ๋งํ์ฌ ๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ๋์ด์.