728x90
📚 목차
- Mock의 개념과 필요성
- Python Mock 기초
- pytest와 mock 고급 활용법
- 웹 프레임워크별 실전 적용
- 테스트 자동화 도구 완전 정복
- 보안과 베스트 프랙티스
1. Mock의 개념과 필요성
🎯 Mock이란?
Mock은 실제 객체의 동작을 모방하는 가짜 객체입니다. 테스트 환경에서 외부 의존성을 제거하고 독립적인 단위 테스트를 가능하게 합니다.
# 실제 코드 - 외부 API에 의존
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
# Mock을 사용한 테스트
from unittest.mock import patch
@patch('requests.get')
def test_get_user_data(mock_get):
# 가짜 응답 설정
mock_get.return_value.json.return_value = {"id": 1, "name": "홍길동"}
# 실제 API 호출 없이 테스트
result = get_user_data(1)
assert result["name"] == "홍길동"
🚀 Mock 사용의 장점
장점 | 설명 |
---|---|
속도 향상 | 네트워크 호출, DB 접근 없이 밀리초 단위 실행 |
안정성 | 외부 서비스 장애와 무관하게 테스트 가능 |
비용 절감 | API 호출 비용, DB 부하 없음 |
재현성 | 동일한 조건에서 항상 같은 결과 |
엣지케이스 테스트 | 에러, 타임아웃 등 특수 상황 시뮬레이션 |
300x250
2. Python Mock 기초
📦 기본 Mock 객체들
from unittest.mock import Mock, MagicMock, patch
# 1. Mock - 기본 mock 객체
mock_obj = Mock()
mock_obj.method.return_value = "결과값"
print(mock_obj.method()) # "결과값"
# 2. MagicMock - 매직 메서드 지원
magic_mock = MagicMock()
magic_mock.__len__.return_value = 5
print(len(magic_mock)) # 5
# 3. side_effect - 연속 호출 또는 예외 처리
mock_func = Mock()
mock_func.side_effect = [1, 2, ValueError("에러!")]
print(mock_func()) # 1
print(mock_func()) # 2
# mock_func() # ValueError 발생
🔧 patch 데코레이터 활용
# 모듈 레벨 함수 패치
@patch('module.function_name')
def test_something(mock_func):
mock_func.return_value = "mocked"
# 테스트 코드
# 컨텍스트 매니저로 사용
def test_with_context():
with patch('os.path.exists') as mock_exists:
mock_exists.return_value = True
# 파일이 존재한다고 가정한 테스트
# 여러 개 동시 패치
@patch('module.func1')
@patch('module.func2')
def test_multiple(mock_func2, mock_func1):
# 순서 주의: 아래부터 위로
pass
3. pytest와 mock 고급 활용법
🧪 pytest-mock 플러그인
pip install pytest-mock
# conftest.py - 공통 fixture 정의
import pytest
from unittest.mock import Mock
@pytest.fixture
def mock_database():
db = Mock()
db.get_user.return_value = {"id": 1, "name": "테스터"}
return db
# test_service.py
def test_user_service(mocker, mock_database):
# mocker는 pytest-mock이 제공하는 fixture
mock_logger = mocker.patch('logging.Logger')
# 서비스 테스트
from services import UserService
service = UserService(mock_database)
user = service.get_user_info(1)
assert user["name"] == "테스터"
mock_logger.info.assert_called_once()
🎭 고급 Mock 패턴
# 1. 속성 접근 체인 mock
def test_chained_calls(mocker):
mock = mocker.Mock()
mock.client.api.users.get.return_value = {"status": "ok"}
result = mock.client.api.users.get()
assert result["status"] == "ok"
# 2. 호출 검증
def test_call_verification(mocker):
mock_func = mocker.Mock()
# 함수 호출
mock_func(1, 2, key="value")
# 다양한 검증 방법
mock_func.assert_called_once_with(1, 2, key="value")
assert mock_func.call_count == 1
assert mock_func.call_args.args == (1, 2)
assert mock_func.call_args.kwargs == {"key": "value"}
# 3. 복잡한 반환값 시뮬레이션
def test_complex_return(mocker):
mock_api = mocker.Mock()
# 호출할 때마다 다른 값 반환
mock_api.get_data.side_effect = [
{"page": 1, "data": [1, 2, 3]},
{"page": 2, "data": [4, 5, 6]},
StopIteration # 더 이상 데이터 없음
]
4. 웹 프레임워크별 실전 적용
🌐 Django 테스트 자동화
# models.py
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
view_count = models.IntegerField(default=0)
# views.py
from django.shortcuts import get_object_or_404
from django.http import JsonResponse
import requests
def article_detail(request, article_id):
article = get_object_or_404(Article, pk=article_id)
# 외부 API로 조회수 기록
requests.post("https://analytics.example.com/track", json={
"article_id": article_id,
"timestamp": datetime.now().isoformat()
})
article.view_count += 1
article.save()
return JsonResponse({
"id": article.id,
"title": article.title,
"content": article.content,
"views": article.view_count
})
# tests.py
from django.test import TestCase, Client
from unittest.mock import patch, Mock
from .models import Article
class ArticleViewTest(TestCase):
def setUp(self):
self.client = Client()
self.article = Article.objects.create(
title="테스트 글",
content="내용",
view_count=0
)
@patch('requests.post')
def test_article_detail_with_tracking(self, mock_post):
# 외부 API 응답 mock
mock_post.return_value.status_code = 200
response = self.client.get(f'/articles/{self.article.id}/')
# 응답 검증
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['title'], '테스트 글')
self.assertEqual(data['views'], 1)
# API 호출 검증
mock_post.assert_called_once()
call_args = mock_post.call_args
self.assertEqual(call_args.args[0], "https://analytics.example.com/track")
⚡ Flask 테스트 자동화
# app.py
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
import redis
app = Flask(__name__)
db = SQLAlchemy(app)
redis_client = redis.Redis()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True)
email = db.Column(db.String(120))
@app.route('/api/users/<int:user_id>')
def get_user(user_id):
# 캐시 확인
cached = redis_client.get(f"user:{user_id}")
if cached:
return jsonify(json.loads(cached))
# DB 조회
user = User.query.get_or_404(user_id)
user_data = {
"id": user.id,
"username": user.username,
"email": user.email
}
# 캐시 저장
redis_client.setex(f"user:{user_id}", 3600, json.dumps(user_data))
return jsonify(user_data)
# test_app.py
import pytest
from unittest.mock import Mock, patch
import json
@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_get_user_from_cache(client, mocker):
# Redis mock
mock_redis = mocker.patch('app.redis_client')
cached_data = json.dumps({"id": 1, "username": "cached_user", "email": "cached@test.com"})
mock_redis.get.return_value = cached_data
response = client.get('/api/users/1')
assert response.status_code == 200
data = response.get_json()
assert data['username'] == 'cached_user'
# DB 조회가 일어나지 않았는지 확인
mock_query = mocker.patch('app.User.query')
mock_query.get_or_404.assert_not_called()
def test_get_user_from_db(client, mocker):
# Redis에 캐시 없음
mock_redis = mocker.patch('app.redis_client')
mock_redis.get.return_value = None
# DB 사용자 mock
mock_user = Mock()
mock_user.id = 1
mock_user.username = "db_user"
mock_user.email = "db@test.com"
mock_query = mocker.patch('app.User.query')
mock_query.get_or_404.return_value = mock_user
response = client.get('/api/users/1')
assert response.status_code == 200
data = response.get_json()
assert data['username'] == 'db_user'
# 캐시 저장 확인
mock_redis.setex.assert_called_once()
5. 테스트 자동화 도구 완전 정복
📊 pytest-cov: 테스트 커버리지 분석
pip install pytest-cov
# 커버리지 실행
pytest --cov=myproject --cov-report=html --cov-report=term
# 특정 파일/폴더 제외
pytest --cov=myproject --cov-omit="*/tests/*,*/migrations/*"
# 최소 커버리지 강제 (80% 미만 시 실패)
pytest --cov=myproject --cov-fail-under=80
# .coveragerc 설정 파일
[run]
source = .
omit =
*/site-packages/*
*/tests/*
*/migrations/*
*/venv/*
[report]
precision = 2
show_missing = True
skip_covered = False
[html]
directory = htmlcov
🔄 tox: 다중 환경 테스트 자동화
pip install tox
# tox.ini
[tox]
envlist = py{38,39,310,311}, lint, docs
skipsdist = True
[testenv]
deps =
pytest
pytest-cov
pytest-mock
-r requirements.txt
commands =
pytest --cov=myproject {posargs}
[testenv:lint]
deps =
flake8
black
isort
mypy
commands =
flake8 myproject tests
black --check myproject tests
isort --check-only myproject tests
mypy myproject
[testenv:docs]
deps =
sphinx
sphinx-rtd-theme
commands =
sphinx-build -b html docs docs/_build
📼 vcrpy: HTTP 상호작용 기록/재생
pip install vcrpy
import vcr
import requests
# 실제 API 호출을 기록하고 재생
@vcr.use_cassette('fixtures/weather_api.yaml')
def test_weather_api():
# 첫 실행: 실제 API 호출하고 응답을 yaml 파일에 저장
# 이후 실행: yaml 파일에서 응답을 읽어옴
response = requests.get('https://api.weather.com/seoul')
assert response.status_code == 200
assert 'temperature' in response.json()
# 고급 설정
my_vcr = vcr.VCR(
serializer='json',
cassette_library_dir='tests/fixtures/cassettes',
record_mode='once', # new_episodes, none, all
match_on=['uri', 'method', 'body'],
filter_headers=['authorization'], # 민감정보 제거
filter_post_data_parameters=['password', 'api_key']
)
with my_vcr.use_cassette('test.json'):
# 테스트 코드
pass
🏭 Factory Boy: 테스트 데이터 생성
pip install factory-boy
import factory
from factory.django import DjangoModelFactory
from myapp.models import User, Article
class UserFactory(DjangoModelFactory):
class Meta:
model = User
username = factory.Sequence(lambda n: f'user{n}')
email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')
first_name = factory.Faker('first_name', locale='ko_KR')
last_name = factory.Faker('last_name', locale='ko_KR')
class ArticleFactory(DjangoModelFactory):
class Meta:
model = Article
title = factory.Faker('sentence', nb_words=5)
content = factory.Faker('text')
author = factory.SubFactory(UserFactory)
published = factory.Faker('date_time_this_year')
# 사용 예시
def test_article_list():
# 10개의 테스트 데이터 생성
articles = ArticleFactory.create_batch(10)
response = client.get('/api/articles/')
assert len(response.json()['results']) == 10
🚦 Hypothesis: 속성 기반 테스트
pip install hypothesis
from hypothesis import given, strategies as st
import re
# 이메일 검증 함수
def is_valid_email(email):
pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
return re.match(pattern, email) is not None
# 속성 기반 테스트
@given(st.emails())
def test_email_validation(email):
assert is_valid_email(email)
# 복잡한 데이터 생성
@given(
username=st.text(min_size=3, max_size=20, alphabet=st.characters(blacklist_characters=" ")),
age=st.integers(min_value=0, max_value=150),
tags=st.lists(st.text(min_size=1), min_size=0, max_size=5)
)
def test_user_creation(username, age, tags):
user = User(username=username, age=age, tags=tags)
assert len(user.username) >= 3
assert 0 <= user.age <= 150
assert isinstance(user.tags, list)
📝 pytest 플러그인 생태계
# 유용한 pytest 플러그인들
pip install pytest-xdist # 병렬 실행
pip install pytest-timeout # 테스트 타임아웃
pip install pytest-benchmark # 성능 벤치마크
pip install pytest-asyncio # 비동기 테스트
pip install pytest-env # 환경변수 설정
# pytest.ini 설정
[tool:pytest]
minversion = 6.0
addopts =
-ra
--strict-markers
--cov=myproject
--cov-branch
--cov-report=term-missing:skip-covered
--cov-fail-under=80
-n auto # xdist 병렬 실행
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 환경변수 설정 (pytest-env)
env =
DATABASE_URL=sqlite:///:memory:
REDIS_URL=redis://localhost:6379/0
SECRET_KEY=test-secret-key
6. 보안과 베스트 프랙티스
🔒 보안 중심 테스트 설계
# 1. 민감정보 마스킹
from unittest.mock import Mock, patch
class SecureAPIClient:
def __init__(self, api_key):
self.api_key = api_key
def make_request(self, endpoint):
headers = {"Authorization": f"Bearer {self.api_key}"}
return requests.get(endpoint, headers=headers)
def test_secure_api_client(mocker):
# API 키를 노출하지 않고 테스트
mock_requests = mocker.patch('requests.get')
mock_requests.return_value.json.return_value = {"status": "success"}
client = SecureAPIClient("FAKE_API_KEY")
result = client.make_request("https://api.example.com/data")
# 호출 검증 시 헤더 확인
call_args = mock_requests.call_args
assert "Bearer FAKE_API_KEY" in call_args.kwargs['headers']['Authorization']
# 2. SQL 인젝션 방지 테스트
def test_sql_injection_prevention(mocker):
mock_db = mocker.Mock()
# 위험한 입력
malicious_input = "'; DROP TABLE users; --"
# 파라미터화된 쿼리 사용 확인
def safe_query(user_id):
return mock_db.execute("SELECT * FROM users WHERE id = ?", (user_id,))
safe_query(malicious_input)
# 안전한 쿼리 실행 확인
mock_db.execute.assert_called_with(
"SELECT * FROM users WHERE id = ?",
("'; DROP TABLE users; --",)
)
✅ 테스트 자동화 CI/CD 통합
# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, 3.10, 3.11]
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install tox tox-gh-actions
- name: Run tests with tox
run: tox
- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
fail_ci_if_error: true
📋 테스트 체크리스트
단위 테스트
- 모든 함수/메서드에 대한 테스트 작성
- 정상 케이스와 예외 케이스 모두 포함
- Mock을 사용한 외부 의존성 격리
- 테스트 커버리지 80% 이상 유지
통합 테스트
- API 엔드포인트 전체 테스트
- 데이터베이스 트랜잭션 테스트
- 외부 서비스 연동 시나리오 (vcrpy 활용)
- 인증/권한 플로우 검증
보안 테스트
- 인증 우회 시도 방어 확인
- SQL 인젝션 방지 검증
- XSS 공격 방어 확인
- 민감정보 노출 방지
성능 테스트
- 응답 시간 벤치마크 (pytest-benchmark)
- 메모리 사용량 모니터링
- 동시성 테스트 (pytest-xdist)
- 부하 테스트 시나리오
완벽한 테스트 자동화는 단순히 테스트를 작성하는 것이 아니라, 체계적인 전략과 적절한 도구의 조합으로 이루어집니다.
- Mock 활용: 외부 의존성을 제거하고 빠르고 안정적인 테스트 구축
- 도구 조합: pytest + mock + coverage + tox + vcrpy 등을 통한 완성도 높은 테스트 환경
- 자동화 통합: CI/CD 파이프라인에 통합하여 지속적인 품질 관리
- 보안 우선: 테스트 단계부터 보안을 고려한 설계
이러한 접근 방식을 통해 100% 자동화된 테스트 환경을 구축하고, 안정적이고 확장 가능한 소프트웨어를 개발할 수 있습니다. 테스트는 단순한 검증이 아닌, 더 나은 코드를 위한 설계 도구임을 잊지 마세요!
728x90
그리드형(광고전용)
댓글