Build a REST API with Django – A Test Driven Approach - Part 1

on under django
12 minute read

Build a REST API with Django – A Test Driven Approach: Part 1

Build a REST API with Django – A Test Driven Approach: Part 1의 포스팅을 번역한 것입니다. 원본의 코드오류(닫지 않은 괄호 등)와 django 버전의 차이로 발생하는 에러를 해결하여 포스팅을 진행하였습니다.

Code without tests is broken as designed. — Jacob Kaplan-Moss

소프트웨어 개발에서 테스트는 중요한 위치에 있습니다. 테스트를 해야 하는 이유는?

  • 테스트는 피드백 루프가 짧기 때문에 팀원과 팀이 더 빨리 학습하고 조정할 수 있습니다.
  • 디버깅에 소요되는 시간이 줄어들어 코드를 작성하는데 더 많은 시간을 할애할 수 있습니다.
  • 테스트는 코드의 문서 역할을 합니다.
  • 버그를 줄이면서 코드 품질을 향상시킵니다.
  • 코드를 리팩터링 한 후, 변경 사항이 이전에 작동중인 코드를 손상시켰는지 여부를 테스트 합니다.
  • 검사를 통해 탈모 방지를 할 수 있습니다. …..

코드 테스트를 수행하는 가장 좋은 방법은 TDD(Test-Driven Development)를 사용하는 것입니다.

table of content

  1. Bucketlist
  2. Django Rest Framework
  3. Rest API 앱 만들기
  4. 코딩 시작하기
  5. serializers
  6. views
  7. URL 처리
  8. Run!
  9. Reading, Updating, Deletion
  10. Wrapping it up
  11. Conclusion

작동 방법

  • 테스트를 작성 : 이 테스트는 앱의 일부 기능을 살핍니다.
  • 테스트 실행 : 테스트를 통과해야하는 코드가 없으므로 테스트가 실패해야 합니다.
  • 코드 작성 : 테스트를 통과하기 위해
  • 테스트 실행 : 통과하면 작성한 코드가 테스트 요구 사항을 충족하고 있다고 확실 할 수 있습니다.
  • 코드 리팩토링 : 중복을 제거하고 큰 개체를 잘라내고 코드를 읽기 쉽게 만듭니다. 코드를 리팩터링 할 때마다 테스트를 다시 실행하십시오.
  • 반복 : 이게 다임!

TDD를 사용하여 버킥 목록 API를 만듭니다. API에는 CRUD와 인증 기능이 있습니다.

Bucketlist

bucketlist는 성취하기를 원하는 모든 목표, 성취하고자 하는 꿈, 죽기전에 경험하고 싶은 일들 등의 목록입니다.

필요한 API 기능들입니다.

  • Bucketlist 만들기
  • Bucketlist 검색
  • 업데이트 및 삭제

필요한 보안 기능입니다.

  • API 사용자 인증
  • Bucketlist 검색중
  • Bucketlist 추가
  • Pagination

Django Rest Framework

DRF는 웹 API를 구축하기위한 강력한 모듈입니다. 인증 정책이 있고, 찾아 볼 수 있는 모델 지원 API를 쉽게 만들 수 있습니다.

왜 DRF인가?

  • 인증 : 기본 및 세션 기반 인증에서 토큰 기반 및 Oauth2 기능에 이르기까지 DRF가 짱입니다.
  • Serializer : ORM과 non-ORM 데이터 소스를 모두 지원하며, 데이터베이스와 통합됩니다.
  • 훌륭한 문서 : 방대한 온라인 설명서와 훌륭한 커뮤니티 지원이 있습니다.
  • Heroku, Mozilla, Red Hat, Eventbrite는 API에서 DRF를 사용합니다.

요구사항

  • Python
  • Django

가상환경 생성 및 Django, DRF 설치는 스킵합니다.

먼저 장고 프로젝트를 생성합니다.

$ django-admin startproject djangorest

생성하면 다음과 같은 폴더 구조를 갖습니다.

├── djangorest
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

DRF 통합

# /djangorest/djangorest/settings.py
...

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles', # Ensure a comma ends this line
    'rest_framework', # Add this line
]

Rest API 앱 만들기

Django는 하나의 응용 프로그램을 구성하기 위해 통합된 여러 개의 응용 프로그램을 만들 수 있습니다. django에 있는 app은 파일을 포함하는 __init__.py 파일 묶음이 있는 파이썬 패키지일 뿐입니다.

api 앱을 생성합니다.

$ python manage.py startapp api

장고 셋팅에 api 앱을 추가합니다.

# /djangorest/djangorest/settings.py
# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api', # Add this line
]

코딩 시작하기

첫째, 우리는 테스트!

모델을 만들어야 하는데, 아직 테스트를 작성하지 않았습니다. 따라서 api 앱의 tests.py 폴더에 몇 가지 테스트를 작성합니다.

# /api/tests.py

from django.test import TestCase
from .models import Bucketlist

class ModelTestCase(TestCase):
    """ 이 클래스는 bucketlist 모델을 위한 test suite를 정의합니다."""
    def setUp(self):
        """ 테스트 클라이언트와 기타 테스트 변수를 정의합니다."""
        self.bucketlist_name = "Write world class code"
        self.bucketlist = Bucketlist(name=self.bucketlist_name)

    def test_model_can_create_a_bucketlist(self):
        """ bucketlist 모델을 테스트하면 bucketlist이 생성될 수 있습니다."""
        old_count = Bucketlist.objects.count()
        self.bucketlist.save()
        new_count = Bucketlist.objects.count()
        self.assertNotEqual(old_count, new_count)

위 코드는 django.test에서 테스트 케이스를 가져옵니다. 테스트 케이스에는 모델이 이름이 있는 bucketlist을 만들 수 있는지 여부를 테스트하는 단일 테스트가 있습니다.

모델을 정의합니다.

# /api/models.py

from django.db import models

class Bucketlist(models.Model):
    pass

test 명령을 사용하여 테스트를 실행합니다.

$ python3 manage.py test

모델 필드를 작성하고 마이그레이션 작업을 하지 않았기 때문에 여러 오류가 발생합니다. Django는 SQlite를 기본 데이터베이스로 사용합니다. 또한 모델을 작성할 때 단일 SQL을 작성할 필요가 없습니다. 모두 장고가 처리합니다.

models.py 파일에 데이터베이스의 테이블 필드를 나타내는 필드를 정의합니다.

# api/models.py

from django.db import models

class Bucketlist(models.Model):
    """ 이 클래스는  모델을 나타냅니다."""
    name = models.CharField(max_length=255, blank=False, unique=True)
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    def __str__(self):
        """사람이 읽을 수 있는 표현으로 모델 인스턴스를 반환합니다."""
        return "{}".format(self.name)

마이그레이션!

마이그레이션은 Django가 데이터베이스 스키마에 모델 변경(모델 추가, 삭제 등)을 적용하는 방식입니다. 풍부한 모델 필드들을 만들었으므로 관련 스키마를 만들도록 데이터베이스에 알려야 합니다.

# 모델에 대한 변경 사항을 기반으로 새로운 마이그레이션을 생성
$ python3 manage.py makemigrations

# 생성된 마이그레이션을 데이터베이스에 적용
$ python3 manage.py migrate

마이그레이션 후 테스트를 실행하면 다음과 같은 내용이 표시됩니다.

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.002s

OK
Destroying test database for alias 'default'...

테스트가 통과되었습니다. 앱용 Serializer를 작성할 수 있습니다.

Serializers

Serializer는 데이터를 직렬화 / 역직력화를 합니다. 직렬화는 DB의 복잡한 쿼리셋에서 JSON이나 XML같은 데이터 형식으로 데이터를 변경합니다. 역직렬화는 DB에 저장하려는 데이터의 유효성을 확인한 후에 프로세스를 되돌립니다.

Model Serializers are awesome!

ModelSerializer 클래스는 자동으로 모델 필드에 해당하는 필드와 시리얼 클래스를 만들 수 있습니다. 이렇게 하면 코드의 량이 쑥 줄어듭니다.

# api/serializers.py

from rest_framework import serializers
from .models import Bucketlist

class BucketlistSerializer(serializers.ModelSerializer):
    """ 모델 인스턴스를 JSON 포멧으로 매핑하는 Serializer입니다."""

    class Meta:
        """ serializer 필드를 모델 필드와 매핑하는 메타 클래스입니다."""
        model = Bucketlist
        fields = ('id', 'name', 'date_created', 'date_modified')
        read_only_fields = ('date_created', 'date_modified')

Views

먼저 뷰의 테스트를 작성합니다. 처음 테스트를 만드는 것은 힘들 수 있습니다. 하지만 무엇을 구현해야하는지를 알면 테스트 할 내용을 쉽게 알 수 있습니다. 여기서는 다음을 처리할 뷰를 생성하려 합니다.

  • bucketlist 만들기 - POST 요청 처리
  • bucketlist 읽기 - GET 요청 처리
  • bucketlist 업데이트 - PUT 요청 처리
  • bucketlist 삭제 - DELETE 요청 처리

위 기능들을 바탕으로 무엇을 테스트해야 하는지 알고 있습니다. 그것들을 지침으로 사용합니다.

첫 번째, API가 bucketlist를 성공적으로 만들지 여부를 테스트 합니다.

# api/tests.py

# Add these imports at the top
from rest_framework.test import APIClient
from rest_framework import status
from django.urls import reverse

# Define this after the ModelTestCase
class ViewTestCase(TestCase):
    """api view를 위한 Test suite입니다. """

    def setUp(self):
        """테스트 클라이언드와 다른 테스트 변수를 정의합니다."""
        self.client = APIClient()
        self.bucketlist_data = {'name': 'Go to Ibiza'}
        self.response = self.client.post(
            reverse('create'),
            self.bucketlist_data,
            format="json")

    def test_api_can_create_a_bucketlist(self):
        """ api에 버킷 생성 기능이 있는지 테스트합니다. """
        self.assertEqual(self.response.status_code, status.HTTP_201_CREATED)

이 테스트는 실패합니다. POST 요청을 처리하기 위한 뷰와 URL을 구현하지 않았기 때문입니다.

계속 views.py를 구현합니다.

# api/views.py

from rest_framework import generics
from .serializers import BucketlistSerializer
from .models import Bucketlist

class CreateView(generics.ListCreateAPIView):
    """이 클래스는 나머지 API의 create 동작을 정의하니다"""
    queryset = Bucketlist.objects.all()
    serializer_class = BucketlistSerializer

    def perform_create(self, serializer):
        """새 bucketlist을 만들때 post 데이터를 저장합니다."""
        serializer.save()

ListCreateAPIViewGET(모든 목록)과 POST 메서드 핸들러를 제공하는 generics view입니다.

queryset과 serializer_class 속성을 지정했습니다. 그리고 한번 게시된 새 bucketlist를 저장하는데 도움을 줄 perform_create 메소드를 선언합니다.

Handling Urls

완성을 위해, API를 사용하기 위한 엔드 포인트로 URL을 지정합니다. URL은 외부와 연결해주는 인터페이스로 생각하면 됩니다. 누군가가 우리의 웹 API와 통신하기 원한다면 우리의 URL을 사용해야 합니다.

url patterns 를 정의합니다.

# api/urls.py

from django.urls import path, include
from rest_framework.urlpatterns import format_suffix_patterns
from .views import CreateView

urlpatterns = {
    path('bucketlists/', CreateView.as_view(), name="create"),
}

urlpatterns = format_suffix_patterns(urlpatterns)

format_suffix_patterns를 사용하면 URL을 사용할 때 데이터형식(원시 json이나 html)을 지정할 수 있습니다. 패턴의 모든 URL에 사용할 형식을 추가합니다.

마지막으로 메인 앱의 urls.py 파일을 추가하여 API 앱을 가리킵니다. 앞에서 선언한 api.urls를 메인 urlpatterns 에 포함시켜야 합니다.

# djangorest/urls.py

from django.conf.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('api.urls')) # Add this line
]

Let’s Run!

django 서버를 실행합니다.

$ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
August 21, 2018 - 04:10:42
Django version 2.0.4, using settings 'djangorest.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

모든 것이 원활하게 동작하고 있음을 의미합니다.

웹 브라우저에서 http://127.0.0.1:8000/bucketlists/으로 접속합니다. 그러면 동작하는 걸 눈으로 볼 수 있습니다.

bucketlist을 작성하고 POST 버튼을 클릭하여 API가 작동하는지 확인합니다.

Reading, Updating and Deletion

Writing the tests

GET, PUT, DELETE 요청을 충족하기 위해 세 가지 테스트를 추가로 작성합니다.

# api/tests.py

    def test_api_can_get_a_bucketlist(self):
        """"API가 주어진 bucketlist을 얻을 수 있는지 테스트합니다."""
        bucketlist = Bucketlist.objects.get()
        response = self.client.get(
                reverse('details', kwargs={'pk': bucketlist.id}),
                    format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertContains(response, bucketlist)

    def test_api_can_update_bucketlist(self):
        """API가 특정 bucketlist을 업데이트 할 수 있는지 테스트합니다."""
        bucketlist = Bucketlist.objects.get()
        change_bucketlist = {'name': 'Something new'}
        res = self.client.put(
            reverse('details', kwargs={'pk': bucketlist.id}),
            change_bucketlist, format='json'
            )
        self.assertEqual(res.status_code, status.HTTP_200_OK)

    def test_api_can_delete_bucketlist(self):
        """API가 bucketlist을 지울 수 있는지 테스트합니다."""
        bucketlist = Bucketlist.objects.get()
        response = self.client.delete(
            reverse('details', kwargs={'pk': bucketlist.id}),
            format='json',
            follow=True)

        self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT)

이 테스트는 실패할 것입니다. 실패를 잡기 위해 PUT, DELETE 메서드 핸들러로 api를 완성해야 합니다. 이것에 대한 뷰 클래스를 정의합니다.

# api/views.py

class DetailsView(generics.RetrieveUpdateDestroyAPIView):
    """이 클래스는 http GET, PUT, DELETE 요청을 처리합니다."""
    queryset = Bucketlist.objects.all()
    serializer_class = BucketlistSerializer

RetrieveUpdateDestroyAPIView는 GET, PUT, PATCH, DELETE 메서드 핸들러를 제공하는 generics view입니다.

마지막으로, DetailsView와 연결할 새 URL을 만듭니다.

# api/urls.py

from .views import DetailsView

path('bucketlists/<pk>/',
        DetailsView.as_view(), name="details"),

Wrapping it up

브라우저로 http://127.0.0.1:8000/bucketlists/1/으로 접속하세요. 짠!! 이제 기존 bucketlist을 편집 할 수 있습니다.

Conclusion

파트 1 끝!!

파트 2에서는 사용자 추가, 권한 부여, 인증 통합, API 문서 작성, 보다 정교한 테스트 추가에 대해 다룹니다.

django
comments powered by Disqus