본문으로 건너뛰기
김신건의 로그

[DRF] Testing: APIClient, APITestCase, force_authenticate

· 수정 · 📖 약 2분 · 613자/단어 #drf #testing #pytest #api-testing
DRF testing, APIClient, APITestCase, APIRequestFactory, force_authenticate, DRF pytest

정의

DRF의 테스트 도구는 Django TestCase 위에 API 특화 helper 를 얹은 것. APIClient 로 real endpoint 를 호출하거나 APIRequestFactory 로 View 단위 테스트가 가능.

테스트 도구 계층

flowchart TD
    Q{테스트 종류}
    Q --> Unit["단위 (Serializer, custom logic)"]
    Q --> Slice["View 단위 (APIRequestFactory)"]
    Q --> Int["통합 (APIClient, DB 포함)"]
    Q --> E2E["End-to-End (playwright, cypress)"]

    Unit --> U1["빠름 (ms)"]
    Slice --> S1["View 로직만"]
    Int --> I1["실제 URL + DB, 권장"]
    E2E --> E1["프론트 포함, 느림"]
도구속도DB
TestCase + Serializer 직접단위ms아니오 (or 필요 시)
APIRequestFactoryView빠름아니오 (mock)
APIClient (DRF)통합중간
Django Client통합 (form only)중간

TIP

DRF 프로젝트는 APIClient 를 기본으로 쓴다. 실제 URL 라우팅 + 인증 + 응답 렌더링 모두 검증.

설치

pip install pytest pytest-django factory-boy freezegun
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings.test
python_files = tests.py test_*.py

1. APIClient (통합 테스트)

from rest_framework.test import APITestCase
from rest_framework import status

class UserAPITest(APITestCase):
    def setUp(self):
        self.user = User.objects.create_user('koa', password='pw')

    def test_list_users(self):
        self.client.force_authenticate(user=self.user)
        response = self.client.get('/api/users/')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data['results']), 1)

    def test_create_requires_auth(self):
        response = self.client.post('/api/users/', {'username': 'new'})
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

self.client = APIClient 인스턴스.

force_authenticate (권장)

# ★ 가장 자주 쓰는 인증 helper
self.client.force_authenticate(user=self.user)
# → JWT 발급, 헤더 첨부 등 우회. 유닛 테스트에 이상적.

self.client.force_authenticate(user=None)  # 로그아웃

DRF 인증 클래스 무시 하고 request.user 를 직접 주입. 가장 빠름.

세션 로그인

self.client.login(username='koa', password='pw')
# 또는
self.client.force_login(self.user)   # Django 스타일

SessionAuthentication 검증까지 하고 싶을 때.

토큰 인증

# TokenAuthentication
token = Token.objects.create(user=self.user)
self.client.credentials(HTTP_AUTHORIZATION=f'Token {token.key}')

# JWTAuthentication
from rest_framework_simplejwt.tokens import RefreshToken
refresh = RefreshToken.for_user(self.user)
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {refresh.access_token}')

실제 인증 파이프라인 을 통과. E2E 시나리오 검증에 적합.

HTTP 메서드

# GET
response = self.client.get('/api/users/', {'q': 'koa'})    # ?q=koa 자동

# POST (JSON 자동)
response = self.client.post('/api/users/', {'username': 'new'}, format='json')

# PATCH
response = self.client.patch('/api/users/1/', {'username': 'updated'}, format='json')

# DELETE
response = self.client.delete('/api/users/1/')

# 헤더
response = self.client.get('/api/users/', HTTP_X_CUSTOM='value')

IMPORTANT

format='json' 명시 권장. 안 하면 form-urlencoded 로 보내져 nested 객체 실패.

응답 검증

def test_get_user(self):
    response = self.client.get('/api/users/1/')

    self.assertEqual(response.status_code, 200)
    self.assertEqual(response.data['username'], 'koa')
    self.assertIn('email', response.data)
    self.assertIsInstance(response.data['groups'], list)

    # 헤더
    self.assertEqual(response['Content-Type'], 'application/json')

    # JSON 직렬화된 문자열
    self.assertEqual(response.json(), {'id': 1, 'username': 'koa', ...})

2. APIRequestFactory (View 단위)

from rest_framework.test import APIRequestFactory

class UserViewTest(TestCase):
    def setUp(self):
        self.factory = APIRequestFactory()
        self.user = User.objects.create_user('koa', password='pw')

    def test_view_directly(self):
        view = UserViewSet.as_view({'get': 'list'})
        request = self.factory.get('/api/users/')
        force_authenticate(request, user=self.user)
        response = view(request)

        self.assertEqual(response.status_code, 200)

URL 라우팅 없이 View 직접 호출. 빠르지만 pipeline 일부만 검증.

3. Serializer 단위 테스트

def test_valid_serializer():
    data = {'username': 'koa', 'email': 'koa@example.com'}
    serializer = UserSerializer(data=data)
    assert serializer.is_valid()
    assert serializer.validated_data['username'] == 'koa'


def test_invalid_email():
    data = {'username': 'koa', 'email': 'not-an-email'}
    serializer = UserSerializer(data=data)
    assert not serializer.is_valid()
    assert 'email' in serializer.errors

4. pytest-django 사용

import pytest

@pytest.fixture
def api_client():
    from rest_framework.test import APIClient
    return APIClient()

@pytest.fixture
def user(db):
    return User.objects.create_user('koa', password='pw')

@pytest.fixture
def auth_client(api_client, user):
    api_client.force_authenticate(user=user)
    return api_client

def test_list_users(auth_client):
    response = auth_client.get('/api/users/')
    assert response.status_code == 200

fixture 조합 이 재사용성 극대화. pytest.ini--reuse-db 로 DB 재생성 없이 반복 실행.

5. Factory Boy (테스트 데이터)

# factories.py
import factory
from django.contrib.auth.models import User

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    username = factory.Sequence(lambda n: f'user{n}')
    email = factory.LazyAttribute(lambda o: f'{o.username}@example.com')
    is_active = True


# 사용
def test_many_users():
    UserFactory.create_batch(50)
    assert User.objects.count() == 50


# 관계
class ArticleFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Article
    title = factory.Faker('sentence')
    author = factory.SubFactory(UserFactory)

fixtures/*.json 대신 factory 로 동적 생성.

응답 status code 매트릭스

@pytest.mark.parametrize('user_type,expected_status', [
    ('anonymous', 401),
    ('regular', 403),
    ('admin', 200),
])
def test_admin_endpoint(api_client, user_type, expected_status):
    if user_type == 'regular':
        api_client.force_authenticate(user=UserFactory())
    elif user_type == 'admin':
        api_client.force_authenticate(user=UserFactory(is_staff=True))

    response = api_client.get('/api/admin/stats/')
    assert response.status_code == expected_status

parametrize권한 매트릭스 완전 커버.

권한 테스트 패턴

class PostPermissionTest(APITestCase):
    def setUp(self):
        self.author = UserFactory()
        self.other = UserFactory()
        self.post = Post.objects.create(author=self.author, title='...')

    def test_author_can_delete(self):
        self.client.force_authenticate(self.author)
        response = self.client.delete(f'/api/posts/{self.post.pk}/')
        self.assertEqual(response.status_code, 204)

    def test_other_cannot_delete(self):
        self.client.force_authenticate(self.other)
        response = self.client.delete(f'/api/posts/{self.post.pk}/')
        self.assertEqual(response.status_code, 403)

자세한 권한 개념은 django-drf-permissions.

Throttle 테스트

from django.core.cache import cache

class ThrottleTest(APITestCase):
    def setUp(self):
        cache.clear()   # ★ throttle 카운트 초기화

    def test_burst_limit(self):
        with self.settings(REST_FRAMEWORK={
            'DEFAULT_THROTTLE_RATES': {'login': '2/min'},
        }):
            for _ in range(2):
                self.assertEqual(self.client.post('/api/login/', ...).status_code, 200)
            self.assertEqual(self.client.post('/api/login/', ...).status_code, 429)

WARNING

setUp 에서 cache.clear() 필수. 안 하면 이전 테스트 카운트 남음.

시간 조작 (freezegun)

from freezegun import freeze_time

@freeze_time('2026-06-25 12:00:00')
def test_expired_token():
    token = RefreshToken.for_user(user)

    with freeze_time('2026-07-25 12:00:00'):    # 30일 후
        response = api_client.get('/api/me/', HTTP_AUTHORIZATION=f'Bearer {token.access_token}')
        assert response.status_code == 401

JWT 만료, 세션 timeout 등 시간 종속 테스트.

Mocking 외부 API

from unittest.mock import patch

@patch('myapp.services.send_email')
def test_signup_sends_email(mock_send):
    response = api_client.post('/api/signup/', {'email': 'a@b.c'})
    assert response.status_code == 201
    mock_send.assert_called_once_with('a@b.c', ...)

DB Snapshot

def test_no_extra_queries():
    # N+1 감시
    with self.assertNumQueries(3):
        response = self.client.get('/api/articles/')

DRF 응답이 예상보다 많은 쿼리를 하면 실패. Performance regression 방어.

자세한 N+1 개념은 django-orm-advanced.

Coverage 측정

pip install coverage
coverage run -m pytest
coverage report
coverage html    # htmlcov/index.html

목표: serializer/view 는 90%+, permission/utility 는 100%.

다른 프레임워크 테스트 도구

Framework테스트 도구
DRFAPIClient, APIRequestFactory, pytest-django
FastAPITestClient (Starlette)
SpringMockMvc, @WebMvcTest
RailsActionDispatch, RSpec, request specs
Expresssupertest
NestJS@nestjs/testing + supertest

흔한 함정

WARNING

  1. format='json' 누락 = form-urlencoded 로 전송, nested 데이터 실패.
  2. cache.clear() 안 함 (Throttle 테스트) = flaky test.
  3. self.client.login() 을 JWT 라우트에 = 세션이라 401. force_authenticate 사용.
  4. setUp 에서 heavy DB 초기화 = 매 테스트마다 실행. fixture 로 격리.
  5. assertEqual(response.data, dict) = OrderedDict 차이로 실패 가능. json.loads(response.content) 로 정규화.

관련 위키

이 글의 용어 (7개)
[Django] 테스트: TestCase, Client, factory_boydjango
정의 Django는 표준 라이브러리 기반 TestCase를 제공한다. 트랜잭션 격리, fixtures, test client, DB 셋업 자동화. 실무는 + 가 사실상 표준. 기…
[Django] DRF 개요: Serializer, ViewSet, Routerdjango
학습 로드맵 이 페이지는 개요. 처음 접하는 사람은 아래 순서로 읽으면 자연스럽다: DRF 는 파이프라인이 핵심. 한번 그림으로 보면 이해가 쉽다: 정의 Django REST F…
[Django] DRF Permission, Throttle, Filterdjango
정의 DRF는 view 호출 전 인증(Authentication) → 권한(Permission) → 스로틀(Throttle) 순으로 검사. 각 단계가 통과해야 view 메서드 실…
[Django] ORM 심화: N+1, prefetch, Subquery, Windowdjango
정의 Django ORM은 SQL을 잘 모르는 개발자에게도 안전하지만 비효율을 숨기기 쉽다. N+1, 비효율 JOIN, 큰 결과셋 메모리 폭발이 흔한 함정. selectrelat…
[DRF] Authentication: Session, Token, JWT, OAuth2django
정의 Authentication = 누가 요청했는지 식별. Authorization () 과 별개. 흐름 자세한 permission 은 . 내장 Authentication Cla…
[DRF] Schemas + OpenAPI: 자동 문서화django
정의 OpenAPI (전 Swagger) = REST API를 기계 가독 스펙으로 기술하는 표준. DRF는 이 스펙을 ViewSet / Serializer 로부터 자동 추출할 수…
[DRF] Tutorial + Quickstart: 첫 API 서버 5분django
정의 Django REST Framework (DRF) 를 처음 사용 하는 사람을 위한 순차 시작 가이드. 5분 안에 동작하는 API 서버 완성. DRF 개요는 . Django …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기