İçeriğe geç

Django Rest ile Test Driven Development

Test Driven Development nedir?

Test Driven Development bir yazılım geliştirme tekniğidir. Türkçe’ye test güdümlü geliştirme olarak çevrilebilir. Süreç olarak bir kaç adım ve bunların kararlı bir şekilde tekrarlanması felsefesine dayanır.

  • Süreç ilk olarak uygulamanın bütün fonksiyonlarının teorik olarak küçük parçalara ayrılması ile başlar.
  • Daha sonra geliştirilmekte olan fonksiyonun testi yazılır.
  • Test çalıştırılır. Bu aşamada normal olarak test hata verecektir. Çünkü kullanılmak istenen fonksiyonun henüz uygulama tarafında bir karşılığı bulunmamaktadır.
  • Fonksiyon testin gereksinimleri karşılayacak şekilde geliştirilir.
  • Test tekrar çalıştırılır. Eğer test geçmiş ise minimum gereksinimler sağlanmıştır. Aksi durumda test edilen kaynak tekrar gözden geçirilerek gereksinimler doğrultusunda genişletilir.
  • Kod eğer imkan var ise tekrar düzenlenir. Optimizasyon, hazır yapıların kullanılması vs vs.
  • Yukarıdaki adımların her fonksiyon için tekrarlanması sonucunda uygulama geliştirilmiş olur.

Neden Test Driven Development?

Test Driven Development ile iş akışını ufak parçalar haline bölebilirsiniz.

Testler yazılır ve kod gereksinimleri karşılayacak şekilde oluşturulur, iyileştirme şansı varsa iyileştirilir. Bu yaklaşım size eklenen her fonksiyonun, özelliğin bir problemi, her zaman en efektik çözüm olmasa da kesinlikle çözdüğünü garanti eder.

Gün sonunda eklenen her özelliğin bir testi vardır. Bu yaklaşım uygulama büyüdükçe, veya yayına çıkmadan önceki olası bugların önüne geçmenize yardımcı olur.

Ekibe daha sonra katılacak bir yazılımcı testleri okuyarak uygulamanın yeteneklerine hızlıca adapte olabilir.

Yazılımları, uygulamaları yaşayan bir organizma olarak düşünür isek teorik olarak belirtilen bütün özellikler iş sonunda eklenmiş ve sınırları o versiyon için çizilmiş olacaktır. Bu da geliştirme maliyetini aşağıya çekecek o versiyon da kullanılmayacak özelliklere bağlı bugların da önüne geçecektir.

Ayrıca her ne olursa olsun neyin yanlış gittiğinden sadece bir test çalıştırma komutu uzaklığındayız.

Zaman kaybı mı?

TDD geliştirme sürecini daha uzun hale mi getirmektedir? Evet. Fakat burada kaybettiğiniz süre size debugging,refactoring maliyetlerinden kar olarak geri dönecek. Yanlış yönde son hız gitmektense bazen yavaşca hedefe gitmek daha iyidir.

Yukarıda da belirttiğim üzere yazılımlar veya uygulamalar yaşayan bir organizma ise sürekli büyüyecek, genişleyecek ve buna bağlı olarak kaynak kodu da genişleyecek.

TDD size sistematik olarak kod ile birlikte büyüyen bir test birikimi de sağlayacaktır. Bug-free!

Tabii ki çok hızlı geliştirme yapmanız gereken durumlarda veya tamami ile test edilmiş hazır yapıları özelliştirmeden kullanacağınız durumlarda bu yaklaşımı bir kenarda tutabilirsiniz. Bir Hackathon’da kimsenin derdi testler olmayacaktır 🙂


Django Rest Framework ve TDD

Django Rest Framework(DRF), Django üzerinde koşan projenize REST mimariye sahip bir API oluşturmak için yerleşik gelen bir çok özelliği ile size yardımcı olur.

  • Browsable API. ( API çerçevinizi göz atılabilir bir halde tarayıcınıza entegre eder. Harici bir uygulama kullanmadan endpointlerinizi HTML formatları kullanarak görebilir, HTML formları sayesinde POST,PUT,PATCH gibi işlemleri kolayca gerçekleştirebilirsiniz.)
  • Geniş kullanıcı doğrulama protokolleri. (Kimlik doğrulama, her uygulamanın en büyük parçalarından biridir. DRF kendi içinde bir çok doğrulama tipini destekler. 3.parti uygulamalar ile daha da genişletilebilir. Token authentication, ve JWT authentication ile alakalı bloglarda tutmuştum.)
  • Django felsefesine dayanan bir çok özellik. ( Serializers native Python data tiplerini XML veya JSON’a çevirmenize yardımcı olur. Generic viewlar ile hızlıca geliştirme yapmanızı sağlar.)
  • İyi dökümantasyon. (DRF’in gerçekten fazlası ile gelişmiş bir dökümantasyonu bulunmakta.)

Talk is cheap. Show me the code. -Linus Torvalds.


Django, DRF kullanarak TDD ile ufak bir uygulama gerçekleştirelim. Kullancılarımız kitaplıklarını dijital ortama aktarabilsinler, ve kitaplarının takibini yapabilsinler.

django-admin startproject library 
cd library/ 
./manage.py startapp book 
code . # opens a visual studio code in pwd.
INSTALLED_APPS = [
    ...
    "rest_framework",
    "rest_framework.authtoken",
    "book",
]
from django.db import models
from django.conf import settings
# Create your models here.
class Book(models.Model):
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="books"
    )
    name = models.CharField(max_length=100)
    author = models.CharField(max_length=200)
    status = models.BooleanField()

    def __str__(self):
        return self.name

Basit bir model oluşturarak devam edelim. Bu aşamada yeni uygulamamız ve  migrationları veritabanına uygulayacağız. Bu yazıda default olarak Sqlite kullanılacaktır. Siz isterseniz farklı bir veritabanı ile çalışabilirsiniz.

./manage.py makemigrations book
./manage.py migrate

Book uygulaması üzerinden gereklilikleri belirleyelim.

  • Kullanıcılar, kimliklerini token ile doğrulayacak. DRF token kullanılacak.
  • Kimliklerini doğrulamadan herhangi bir kaynağa erişemeyecekler.
  • Doğrulama sonrası kitap ekleyebilecek, kendine ait olanları listeleyebilecek, silebilecek ve düzenleyebilecek. (Hepimiz bir yerlerde CRUD yapıyoruz değil mi? 🙂 )

Let’s test!

İlk olarak test sınıflarını oluşturalım. Daha sonra test aşamalarını yazacağız.

  1. Doğrulanmamış kullanıcının reddi.
  2. Doğrulanmış kullanıcının kitaplarını listeleme.
  3. Var olan test datalarına API endpointi üzerinden kitap ekleme.
  4. Var olan kitapları API endpoint üzerinden düzenleme.
  5. Filtreleme ve serializasyonların doğru yapılıp yapılmadığının kontrolü.
from book.api.serializers import BookSerializer
from django.urls import reverse
from django.test import TestCase
from rest_framework.test import APIClient
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
from rest_framework import status
from book.models import Book
from rest_framework import serializers


def populate_book_url(book=None):
    if book is None:
        return reverse("book:book-list")
    return reverse("book:book-detail", kwargs={"book_id": book.id})


class PublicBookApiTests(TestCase):
    def setUp(self):
        self.client = APIClient()

    def test_get_books_unauthenticated(self):
        url = populate_book_url()
        res = self.client.get(url)
        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)


class PrivateBookApiTests(TestCase):
    def setUp(self):
        self.client = APIClient()

    @classmethod
    def setUpTestData(cls):
        cls.user = User.objects.create_user(username="egehan", password="supersecret")
        cls.user_2 = User.objects.create_user(
            username="gundogdu", password="superpassword"
        )
        cls.token = Token.objects.create(user=cls.user)
        cls.token = Token.objects.create(user=cls.user_2)
        """setting up test data. user1 book"""
        cls.book = Book.objects.create(
            owner=cls.user, name="1984", author="George Orwell", status=True
        )
        cls.book1 = Book.objects.create(
            owner=cls.user,
            name="Harry Potter Deathly Hallows",
            author="J.K Rowling",
            status=False,
        )

        """user 2 book"""
        cls.book2 = Book.objects.create(
            owner=cls.user_2,
            name="Computer Networks and Internets",
            author="Dogulas E. Comer",
            status=False,
        )

    def test_list_owned_books(self):
        url = populate_book_url()

        self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.user.auth_token.key}")
        res = self.client.get(url)
        serializer = BookSerializer(
            instance=Book.objects.filter(owner=self.user), many=True
        )
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data, serializer.data)

    def test_create_new_book(self):
        url = populate_book_url()
        self.client.credentials(HTTP_AUTHORIZATION=f"Token {self.user.auth_token.key}")
        #  or self.client.force_authenticate(self.user, self.user.auth_token.key)
        payload = {
            "name": "Harry Potter Half Blood Prince",
            "author": "J.K Rowling",
            "status": True,
        }
        res = self.client.post(url, data=payload, format="json")

        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Book.objects.filter(owner=self.user).count(), 3)
        self.assertEqual(Book.objects.last().name, payload["name"])

    def test_update_owned_book(self):
        url = populate_book_url(self.book)
        self.client.force_authenticate(self.user)
        payload = {"status": False}

        res = self.client.patch(url, data=payload, format="json")

        self.book.refresh_from_db()
        self.assertFalse(self.book.status)

    def test_get_not_owned_book(self):
        url = populate_book_url(self.book2)

        self.client.credentials(
            HTTP_AUTHORIZATION=f"Token {self.user.auth_token.key}"
        )  # authenticate user1

        res = self.client.get(url)
        self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)

    def test_create_new_book_wrong_payload(self):
        wrong_payload = {"name": "missed author name", "status": False}
        with self.assertRaisesMessage(
            serializers.ValidationError, "This field is required"
        ):
            serializer = BookSerializer(data=wrong_payload)
            serializer.is_valid(raise_exception=True)

    def test_delete_owned_book(self):
        self.client.credentials(
            HTTP_AUTHORIZATION=f"Token {self.user_2.auth_token.key}"
        )  # authenticate user2
        url = populate_book_url(self.book2)
        res = self.client.delete(url)
        self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
        self.assertEqual(Book.objects.filter(owner=self.user_2).count(), 0)

Testleri çalıştırdığınızda beklenildiği üzere hata alacaksınız. Çünkü henüz bir serializera, api isteklerini karşılayacak bir view ve main url dosyasından book uygulamasını bir yönlendirme tanımlamadık. Testi geçebilmek için sırası ile bu adımları yapalım.

Book uygulaması içinde api adlı bir klasör oluşturun. Daha sonra serializers.py.

from rest_framework import serializers
from book.models import Book

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ["name", "author"]
        read_only_fields = ("owner", "id")

book/api/views.py

from rest_framework import viewsets, permissions, authentication
from rest_framework import mixins
from book.models import Book
from book.api import serializers


class BookViewSet(viewsets.GenericViewset,
                  mixins.CreateModelMixin,
                  mixins.RetriveModelMixin,
                  mixins.UpdateModelMixin,
                  mixins.DestroyModelMixin,
                  mixins.ListModelMixin):
    """manages the book"""

    queryset = Book.objects.all()
    permission_classes = (permissions.IsAuthenticated,)
    authentication_classes = (authentication.TokenAuthentication,)
    serializer_class = serializers.BookSerializer
    lookup_url_kwarg = "book_id"

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

    def get_queryset(self):
        return self.queryset.filter(owner=self.request.user)

book/api/urls.py

from django.urls import path, include
from rest_framework import routers
from book.api.views import BookViewSet

router = routers.DefaultRouter()
router.register(r"books", BookViewSet)
app_name = "book"
urlpatterns = [path("", include(router.urls))]

library/urls.py

from django.urls import path, include

urlpatterns = [
    ...
    path("api/", include("book.api.urls")),
]

Testleri tekrar çalıştırın. Bu sefer geçecek. Testlerin gereksinimini şu anlık karşılamakta.

Yukarıda yapılanları hızlıca özetleyecek olursam, Book modelinin nesnelerini JSON veya XML formatına dönüştürmek için bir serializer tanımladık.

Daha sonra gelecek istekleri karşılamak için bir API viewi yazdık. DRF’te bu işi yapmanın bir çok yolu bulunmakta. Burada viewsetler kullanıldı. Generic viewset ve mixin olarak listmodelmixinden türetildi.

Bu view için kullanıcı doğrulaması olarak TokenAuthentication kullanılacağını belirttik. Ve sadece doğrulanmış kullanıcıların bu viewa erişimi olacağını belirttik.

Viewsetler diğer url endpointleri için router adlı bir kavram ile birlikte gelmekte. Bizim için kaydedilen viewsetin url endpointlerini otomatik olarak yönetmekte.

Main url dosyasından book uygulamasına yönlendirme yapıldı.

Devam edelim.


Testlerin gereksinimleri şu anda karşılanmakta.

Bu yaklaşım belki yavaş gibi gözükebilir fakat ileride uygulama beklenmeyen davranışlar gösterdiğinde debug edip problemi çözmekten daha pragmatik ve basit.

KISS 🙂 (Keep It Simple Stupid)


Son adım olarakta test edilen kaynağın iyileştirilebileceğinden bahsetmiştik.

class BookViewSet(viewsets.ModelViewSet):
    """manages the book"""

    ...

ModelViewSet kullanarak hazır bir yapı kullanarak mixin kalabalığından kurtulmuş olduk.


Son sözler.

Eğer TDD yaklaşımı ile bir uygulama geliştiriyor iseniz herhangi bir Continuous Integration tooluna bağlamak fazlasıyla hayat kurtarıcı olabilir.

Uygulama üzerindeki değişikliklerin deployment anında kullanılacak olan teknolojilerle birlikte çalışabildiğini de garanti altına almış olur, hızlı dağıtım yaparsınız.

Tabii ki Continuous Integration yaklaşımı sadece bununla sınırlı değil.

Fakat konuyu fazla dağıtacağını düşündüğümden burada kesiyorum.

Artık TDD ve DRF ile bu yaklaşımın nasıl uygulanbileceği hakkında fikir sahibisiniz.

Görüşmek üzere!

 

 

 

 

 

 

 

 

Tarih:Blog

İlk Yorumu Siz Yapın

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.

Göster
Gizle