KR_UnitTest - somaz94/python-study GitHub Wiki

Python ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฐœ๋… ์ •๋ฆฌ


1๏ธโƒฃ unittest ๊ธฐ์ดˆ

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์—์„œ ์˜๊ฐ์„ ๋ฐ›์€ ์„ค๊ณ„


2๏ธโƒฃ pytest ์‚ฌ์šฉํ•˜๊ธฐ

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์™€์˜ ํ˜ธํ™˜์„ฑ ์ง€์›
  • ๋งˆ์ปค๋ฅผ ํ†ตํ•œ ํ…Œ์ŠคํŠธ ๋ถ„๋ฅ˜ ๋ฐ ์„ ํƒ์  ์‹คํ–‰
  • ๋ณ‘๋ ฌ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ง€์›์œผ๋กœ ์‹คํ–‰ ์‹œ๊ฐ„ ๋‹จ์ถ•


3๏ธโƒฃ ๋ชจ์˜ ๊ฐ์ฒด(Mock) ์‚ฌ์šฉ

๋ชจ์˜ ๊ฐ์ฒด(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)

์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” Mock ๊ด€๋ จ ๊ธฐ๋Šฅ

  1. MagicMock: __getitem__, __len__ ๋“ฑ ํŠน์ˆ˜ ๋ฉ”์„œ๋“œ๋ฅผ ์ž๋™์œผ๋กœ ๊ตฌํ˜„ํ•œ Mock ํ™•์žฅ
  2. patch ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ/์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ์ž: ์ง€์ •๋œ ๊ฐ์ฒด๋ฅผ ์ผ์‹œ์ ์œผ๋กœ ๋ชจ์˜ ๊ฐ์ฒด๋กœ ๋Œ€์ฒด
  3. side_effect: ์—ฌ๋Ÿฌ ํ˜ธ์ถœ์— ๋Œ€ํ•œ ๋‹ค์–‘ํ•œ ๊ฒฐ๊ณผ๋‚˜ ๋™์ž‘ ์ง€์ •
  4. assert_called_with: ํŠน์ • ์ธ์ž๋กœ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
  5. assert_has_calls: ์—ฌ๋Ÿฌ ํ˜ธ์ถœ ํŒจํ„ด ๊ฒ€์ฆ
  6. return_value: ๋ชจ์˜ ๊ฐ์ฒด๊ฐ€ ๋ฐ˜ํ™˜ํ•  ๊ฐ’ ์„ค์ •
  7. autospec: ์›๋ณธ ๊ฐ์ฒด์˜ API๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๋„๋ก ๋ชจ์˜ ๊ฐ์ฒด ์ƒ์„ฑ

โœ… ํŠน์ง•:

  • ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•˜์—ฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์˜ ์‹ ๋ขฐ์„ฑ ํ–ฅ์ƒ
  • ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์žฌํ˜„ํ•˜๊ธฐ ์–ด๋ ค์šด ์ƒํ™ฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๊ฐ€๋Šฅ
  • ํ•จ์ˆ˜๋‚˜ ๋ฉ”์„œ๋“œ์˜ ํ˜ธ์ถœ ์—ฌ๋ถ€, ํšŸ์ˆ˜, ์ธ์ž ๋“ฑ์„ ์ƒ์„ธํžˆ ๊ฒ€์ฆ
  • ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ์œ ์—ฐํ•œ ์‘๋‹ต ์„ค์ •
  • ์‹ค์ œ ๊ตฌํ˜„์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์€ ์ปดํฌ๋„ŒํŠธ๋„ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
  • ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์†๋„ ํ–ฅ์ƒ (์‹ค์ œ API๋‚˜ DB ์—ฐ๊ฒฐ ์—†์ด ํ…Œ์ŠคํŠธ)
  • ํŠน์ˆ˜ํ•œ ์ƒํ™ฉ(๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜, ํƒ€์ž„์•„์›ƒ ๋“ฑ)์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์šฉ์ด


4๏ธโƒฃ ํ…Œ์ŠคํŠธ ์‹คํ–‰๊ณผ ๋””๋ฒ„๊น…

ํ…Œ์ŠคํŠธ ์‹คํ–‰๊ณผ ๋””๋ฒ„๊น…์€ ํšจ์œจ์ ์ธ ํ…Œ์ŠคํŠธ ํ”„๋กœ์„ธ์Šค๋ฅผ ์œ„ํ•œ ํ•ต์‹ฌ ์š”์†Œ์ด๋‹ค. ํ…Œ์ŠคํŠธ ์ „ํ›„ ์ƒํƒœ ์„ค์ •๊ณผ ์ •๋ฆฌ, ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๋ถ„์„์„ ํ†ตํ•ด ์ฝ”๋“œ์˜ ์‹ ๋ขฐ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ค.

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 ๋“ฑ)
  • ๋””๋ฒ„๊น… ๋„๊ตฌ์™€์˜ ํ†ตํ•ฉ์„ ํ†ตํ•œ ๋ฌธ์ œ ํ•ด๊ฒฐ ์ง€์›
  • ํ…Œ์ŠคํŠธ ์‹คํŒจ ์›์ธ ํŒŒ์•…์„ ์œ„ํ•œ ๋‹ค์–‘ํ•œ ๋„๊ตฌ ์ œ๊ณต


5๏ธโƒฃ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€์™€ ํ’ˆ์งˆ ๊ด€๋ฆฌ

ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ์ฝ”๋“œ๊ฐ€ ํ…Œ์ŠคํŠธ๋กœ ์–ผ๋งˆ๋‚˜ ์ž˜ ๊ฒ€์ฆ๋˜์—ˆ๋Š”์ง€ ์ธก์ •ํ•˜๋Š” ์ง€ํ‘œ์ด๋‹ค. ๋†’์€ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ๋ฒ„๊ทธ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ์„ ์ค„์ด๊ณ  ์ฝ”๋“œ ํ’ˆ์งˆ์„ ํ–ฅ์ƒ์‹œํ‚จ๋‹ค.

ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€์˜ ์ฃผ์š” ์œ ํ˜•์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค:

  • ๊ตฌ๋ฌธ ์ปค๋ฒ„๋ฆฌ์ง€: ๊ฐ ์ฝ”๋“œ ๋ผ์ธ์ด ์‹คํ–‰๋˜๋Š”์ง€ ์ธก์ •
  • ๋ถ„๊ธฐ ์ปค๋ฒ„๋ฆฌ์ง€: ๋ชจ๋“  ์กฐ๊ฑด๋ถ€ ๋ถ„๊ธฐ๊ฐ€ ์‹คํ–‰๋˜๋Š”์ง€ ์ธก์ •
  • ๊ฒฝ๋กœ ์ปค๋ฒ„๋ฆฌ์ง€: ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์‹คํ–‰ ๊ฒฝ๋กœ๊ฐ€ ํ…Œ์ŠคํŠธ๋˜๋Š”์ง€ ์ธก์ •
  • ํ•จ์ˆ˜ ์ปค๋ฒ„๋ฆฌ์ง€: ๊ฐ ํ•จ์ˆ˜๋‚˜ ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๋Š”์ง€ ์ธก์ •
# ํ…Œ์ŠคํŠธ ๋Œ€์ƒ ์ฝ”๋“œ
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)

โœ… ํŠน์ง•:

  • ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ • ๋„๊ตฌ๋ฅผ ํ†ตํ•œ ํ…Œ์ŠคํŠธ ๋ฒ”์œ„ ์‹œ๊ฐํ™”
  • ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ํ…Œ์ŠคํŠธ๋กœ ๋‹ค์–‘ํ•œ ์ž…๋ ฅ ๊ฒ€์ฆ
  • ์†์„ฑ ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ๋กœ ๊ทœ์น™ ๋ฐ ๋ถˆ๋ณ€ ์กฐ๊ฑด ๊ฒ€์ฆ
  • ๊ฒฝ๊ณ„๊ฐ’ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•œ ์ทจ์•ฝ ์ง€์  ์ง‘์ค‘ ๊ฒ€์ฆ
  • ํ…Œ์ŠคํŠธ ๋ณด๊ณ ์„œ ์ž๋™ํ™”๋กœ ์ง€์†์ ์ธ ํ’ˆ์งˆ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๋ฏธ๊ฒ€์ฆ ์ฝ”๋“œ ์˜์—ญ ์‹๋ณ„์„ ํ†ตํ•œ ํ…Œ์ŠคํŠธ ๊ฐœ์„ 
  • ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์ž๋™ํ™”๋ฅผ ํ†ตํ•œ ํšจ์œจ์„ฑ ํ–ฅ์ƒ


6๏ธโƒฃ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•จ๊ป˜ ๋™์ž‘ํ•  ๋•Œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ํ…Œ์ŠคํŠธ์ด๋‹ค. ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์™€ ๋‹ฌ๋ฆฌ ์‹ค์ œ ์˜์กด์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ์Šคํ…œ ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ์„ ํ…Œ์ŠคํŠธํ•œ๋‹ค.

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์˜ ๋ชฉ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค:

  • ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ์ธํ„ฐํŽ˜์ด์Šค ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ
  • ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ •ํ™•์„ฑ ํ™•์ธ
  • ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ์˜ ์˜ฌ๋ฐ”๋ฅธ ํ†ตํ•ฉ ํ™•์ธ
  • ์‹ค์ œ ํ™˜๊ฒฝ๊ณผ ์œ ์‚ฌํ•œ ์กฐ๊ฑด์—์„œ์˜ ๋™์ž‘ ๊ฒ€์ฆ
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 ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

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)

โœ… ํŠน์ง•:

  • ์ปดํฌ๋„ŒํŠธ ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ ๋ฐ ํ†ตํ•ฉ ๊ฒ€์ฆ
  • ์‹ค์ œ ๋˜๋Š” ๋ชจ์˜ ์™ธ๋ถ€ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•œ ํ…Œ์ŠคํŠธ
  • ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค ๊ฒ€์ฆ
  • ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ๋ฐ ์ „์†ก ์ •ํ™•์„ฑ ํ™•์ธ
  • ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์˜ˆ์™ธ ์ƒํ™ฉ ํ…Œ์ŠคํŠธ
  • ์‹ค์ œ ํ™˜๊ฒฝ๊ณผ ์œ ์‚ฌํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐ๋ฐ˜ ํ…Œ์ŠคํŠธ
  • ์„ฑ๋Šฅ, ๋ณด์•ˆ, ์‹ ๋ขฐ์„ฑ ๋“ฑ์˜ ๋น„๊ธฐ๋Šฅ์  ์š”๊ตฌ์‚ฌํ•ญ ๊ฒ€์ฆ


์ฃผ์š” ํŒ

โœ… ๋ชจ๋ฒ” ์‚ฌ๋ก€:

  1. ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ ์œ ์ง€: ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ฐ„ ์˜์กด์„ฑ์„ ๋ฐฉ์ง€ํ•˜์ž.

  2. ๋ช…ํ™•ํ•œ ํ…Œ์ŠคํŠธ ์ด๋ฆ„ ์‚ฌ์šฉ: ํ…Œ์ŠคํŠธ ์ด๋ฆ„์€ ๋ฌด์—‡์„ ํ…Œ์ŠคํŠธํ•˜๋Š”์ง€ ๋ช…ํ™•ํžˆ ํ‘œํ˜„ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ์‹คํŒจ ์‹œ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์ž.

  3. ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ๋Š” ํ•˜๋‚˜์˜ ๋™์ž‘๋งŒ ๊ฒ€์ฆ: ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋‹จ์ผ ๊ธฐ๋Šฅ์ด๋‚˜ ๋™์ž‘์— ์ง‘์ค‘ํ•˜์—ฌ ๋ณต์žก์„ฑ์„ ์ค„์ด๊ณ  ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ด์ž.

  4. ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ฒ˜ ํ™œ์šฉ: ๊ณตํ†ต ์„ค์ • ์ฝ”๋“œ๋ฅผ ํ”ฝ์Šค์ฒ˜๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ฝ”๋“œ ์ค‘๋ณต์„ ์ค„์ด๊ณ  ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์ด์ž.

  5. ์ ์ ˆํ•œ assertion ์‚ฌ์šฉ: ์ƒํ™ฉ์— ๋งž๋Š” ๊ตฌ์ฒด์ ์ธ assertion์„ ์„ ํƒํ•˜์—ฌ ์‹คํŒจ ๋ฉ”์‹œ์ง€์˜ ๋ช…ํ™•์„ฑ์„ ๋†’์ด์ž.

  6. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๊ด€๋ฆฌ: ์ค‘์š”ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ๋†’์€ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์œ ์ง€ํ•˜์—ฌ ๋ฒ„๊ทธ ๋ฐœ์ƒ ๊ฐ€๋Šฅ์„ฑ์„ ์ค„์ด์ž.

  7. CI/CD ํ†ตํ•ฉ: ์ง€์†์  ํ†ตํ•ฉ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž๋™์œผ๋กœ ์‹คํ–‰ํ•˜์—ฌ ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์ž.

  8. ํ…Œ์ŠคํŠธ ์†๋„ ์ตœ์ ํ™”: ๋А๋ฆฐ ํ…Œ์ŠคํŠธ๋Š” ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์„ ์ €ํ•˜์‹œํ‚ค๋ฏ€๋กœ, ๋น ๋ฅด๊ฒŒ ์‹คํ–‰๋˜๋„๋ก ์ตœ์ ํ™”ํ•˜์ž.

  9. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ: ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์„ค์ •ํ•˜๊ณ  ํ…Œ์ŠคํŠธ ํ›„ ์ •๋ฆฌํ•˜์—ฌ ํ™˜๊ฒฝ ์˜ค์—ผ์„ ๋ฐฉ์ง€ํ•˜์ž.

  10. ์ •๊ธฐ์ ์ธ ๋ฆฌํŒฉํ† ๋ง: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ ์ •๊ธฐ์ ์œผ๋กœ ๋ฆฌํŒฉํ† ๋งํ•˜์—ฌ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ด์ž.



โš ๏ธ **GitHub.com Fallback** โš ๏ธ