İçeriğe geç →

Python (Django) ile Test Odaklı Geliştirme (TDD) – 3

Önceki bölümlerde; Python (Django) ile Test Odaklı Geliştirme (TDD) – 2

Bölüm 1:  TDD Temelleri ve Django (Part 1: The Basics of TDD and Django)

​Kısım 3: Unit Test ile Basit Bir Ana Sayfa Testi (Testing a Simple Home Page with Unit Tests)

FT’i ve Unit Test’i iyice netleştirmek için şöyle düşünebiliriz;

  • FT, uygulamayı dışarıdan, son kullanıcı gözünden test etmek için,
  • Unit Test, uygulamayı içeriden, yazılımcının gözünden test için.

Uygulamamızı yaparken hem FT hem de Unit Test kullanacağız. Çalışma adımlarımız hemen hemen şu şekilde olacak;

  1. FT yazarak başlıyoruz, kullanıcının gözünden uygulamanın yeni bir işlevini tanımlıyoruz, açıklıyoruz.
  2. FT’miz başarısız olduğunda, bu hatayı çözecek kodu nasıl yazacağımızı düşünüyoruz. Kodumuzu test etmek için bir ya da daha fazla Unit Test yazabiliriz. Buradaki amaç, uygulama için yazdığımız her bir satırın, en azından bir Unit Test tarafından testten geçirilmiş olması.
  3. Unit Test de hata verdiğinde, geçerli hatayı geçecek kadar, uygulamamız için kod yazıyoruz. FT’nin biraz daha ilerleyebileceğini düşündüğümüz ana kadar, 2. ve 3. adımları birkaç kez tekrar edebiliriz.
  4. FT’nin geçip geçmediğini görmek için FT’mizi tekrar çalıştırıyoruz. Duruma göre yeni Unit Testler, yeni uygulama kodları yazabiliriz.

FT ve Unit Testler’den birisi gereksiz diye düşünebilirsiniz. Fakat her ikisinin de oldukça farklı amaçları olduğunu unutmamak gerek.

  • FT’ler uygulamamızın doğru işlevselliğe sahip olduğundan ve yanlışlıkla onları bozmadığımızdan emin olmamızı sağlar.
  • Unit testler ise temiz ve hatasız kodlar yazmamıza yardımcı olurlar.

Yeni bir Django “app”i oluşturarak devam edelim. Adı “lists” olacak.

$ python manage.py startapp lists

Proje dizini hemen hemen şöyle gözükmeli;

.
├── db.sqlite3
├── functional_tests.py
├── lists
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── manage.py
├── superlists
│   ├── __init__.py
│   ├── __pycache__
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── virtualenv
    ├── [...]

lists/tests.py dosyamızı açalım. Django’nun bizim için TestCase sınıfını import ettiğini görebiliriz. Bu TestCase, Django’ya özel birkaç özelliğin eklendiği bir sınıf.

from django.test import TestCase

# Create your tests here.

Testimizi yazmaya başlamadan önce, isterseniz burada commit yapabilirsiniz.

Öncelikle iki şeyi test etmek istiyoruz;

  • Ana sayfa için olan URL (“/”), bizim istediğimiz view fonksiyonunu döndürüyor mu?
  • Bu view fonksiyonu, FT’den geçecek HTML sayfasını döndürüyor mu?

lists/tests.py dosyasını şu şekilde güncelleyelim;

from django.test import TestCase
from django.urls import resolve

from lists.views import home_page  

class HomePageTest(TestCase):

    def test_root_url_resolves_to_home_page_view(self):
        found = resolve('/')  
        self.assertEqual(found.func, home_page)

4.satırdaki from lists.views import home_page ifadesi kafanızı karıştırmasın. Henüz bu view fonksiyonunu yazmadık ama şimdiden anlayabiliriz ki, istediğimiz HTML sayfasını bu fonksiyonun döndürmesini planlıyoruz. Testimizi çalıştıralım (doğal olarak hata bekliyoruz);

$ python manage.py test
ImportError: cannot import name 'home_page'

Şimdi biraz uygulama kısmı için kod yazmaya başlayabiliriz. Ama önce TDD ne demekti hatırlayın; TDD demek, her seferinde bir adım atmak demekti. Ufak güzel adımlar…

O zaman ne yapıyoruz? Sadece aldığımız hatayı geçecek kadar kod yazıyoruz;

lists/views.py dosyasını açalım ve düzenleyelim;

from django.shortcuts import render

# Create your views here.
home_page = None

Evet, sadece bu kadar. Şimdi testimizi tekrar çalıştıralım;

$ python manage.py test
Creating test database for alias 'default'...
E
======================================================================
ERROR: test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest)
 ---------------------------------------------------------------------
Traceback (most recent call last):
  File "...python-tdd-book/lists/tests.py", line 8, in
test_root_url_resolves_to_home_page_view
    found = resolve('/')
  File ".../django/urls/base.py", line 27, in resolve
    return get_resolver(urlconf).resolve(path)
  File ".../django/urls/resolvers.py", line 392, in resolve
    raise Resolver404({'tried': tried, 'path': new_path})
django.urls.exceptions.Resolver404: {'tried': [[<RegexURLResolver
<RegexURLPattern list> (admin:admin) ^admin/>]], 'path': ''}

 ---------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)
System check identified no issues (0 silenced).
Destroying test database for alias 'default'...

Farklı bir hata aldık. Yeni küçük güzel adımlar atma zamanı. Hata, aradığımız path‘in bulunamadığını söylüyor. O zaman, superlists/urls.py dosyasını açıp düzenleyelim;

from django.conf.urls import url
from django.contrib import admin

from lists import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^$', views.home_page, name='home'),
]

Ve testimizi tekrar çalıştıralım;

$ python manage.py test
[...]
TypeError: view must be a callable or a list/tuple in the case of include().

404 hatası yerine yeni bir hatamız var. Harika!

Hatanın söylediği, view‘ın (home_page) geçersiz bir tipte olduğu. O zaman bunu düzeltelim;

lists/views.py

from django.shortcuts import render

# Create your views here.
def home_page():
    pass

Tekrar testimizi çalıştırma zamanı 🙂 .

$ python manage.py test
Creating test database for alias 'default'...
.
 ---------------------------------------------------------------------
Ran 1 test in 0.003s

OK
System check identified no issues (0 silenced).
Destroying test database for alias 'default'...
Ne?! Hata yok mu? Test geçmiş mi?!

O zaman hemen bi commit yapalım: git commit -am "First unit test and url mapping, dummy view"

Şimdi, hiçbir şey yapmayan bu view fonksiyonunu, HTML sayfası döndürecek hale sokalım;

lists/tests.py

from django.urls import resolve
from django.test import TestCase
from django.http import HttpRequest

from lists.views import home_page


class HomePageTest(TestCase):

    def test_root_url_resolves_to_home_page_view(self):
        found = resolve('/')
        self.assertEqual(found.func, home_page)


    def test_home_page_returns_correct_html(self):
        request = HttpRequest()  
        response = home_page(request)  
        html = response.content.decode('utf8')  
        self.assertTrue(html.startswith('<html>'))  
        self.assertIn('<title>To-Do lists</title>', html)  
        self.assertTrue(html.endswith('</html>'))

Testimizi çalıştıralım;

TypeError: home_page() takes 0 positional arguments but 1 was given

Döngümüzü hatırlayalım;

  • Unit test’i çalıştır ve nasıl hata verdiğini gör
  • Testi geçecek kadar minimal kod değişikliği yap
  • Tekrar et

İşte hatamızı çözecek kadar gereken minimal kod değişikliği;

lists/views.py

def home_page(request):
    pass

Test:

html = response.content.decode('utf8')
AttributeError: 'NoneType' object has no attribute 'content'

Kod değişikliği;

lists/views.py

from django.http import HttpResponse

# Create your views here.
def home_page(request):
    return HttpResponse()

Test:

    self.assertTrue(html.startswith('<html>'))
AssertionError: False is not true

Kod;

lists/views.py

def home_page(request):
    return HttpResponse('<html>')

Test:

AssertionError: '<title>To-Do lists</title>' not found in '<html>'

Kod:

lists/views.py

def home_page(request):
    return HttpResponse('<html><title>To-Do lists</title>')

Test:

    self.assertTrue(html.endswith('</html>'))
AssertionError: False is not true

Kod:

lists/views.py

def home_page(request):
    return HttpResponse('<html><title>To-Do lists</title></html>')

Test:

$ python manage.py test
Creating test database for alias 'default'...
..
 ---------------------------------------------------------------------
Ran 2 tests in 0.001s

OK
System check identified no issues (0 silenced).
Destroying test database for alias 'default'...
Ne?! Hata yok mu! Bitti mi?!

Hayır, bitmedi. FT çalıştırmaya geldi sıra. runserver komutunu farklı bir terminalde çalıştırmayı unutmayın önce.

$ python functional_tests.py
F
======================================================================
FAIL: test_can_start_a_list_and_retrieve_it_later (__main__.NewVisitorTest)
 ---------------------------------------------------------------------
Traceback (most recent call last):
  File "functional_tests.py", line 19, in
test_can_start_a_list_and_retrieve_it_later
    self.fail('Finish the test!')
AssertionError: Finish the test!

 ---------------------------------------------------------------------
Ran 1 test in 1.609s

FAILED (failures=1)

Yine mi hata? Minimal kod değişikliği yapmaya mı başlıyoruz?

Hayır 😀 . Evet, FT başarısız oldu ama hata veren kod satırı; self.fail('Finish the test!'). Bu satırın amacı zaten, hata verdirmek. Testimizin bittiğini anlamak için biz yazmıştık.

Son bir kez daha commit yaptıktan sonra bu kısmı da bitirebiliriz.

$ git commit -am "Basic view now returns minimal HTML"

Not;

Bu yazı, Test-Driven Development with Python: Obey the Testing Goat: Using Django, Selenium, and JavaScript kitabının, yazarından da onayı alınarak, geniş şekilde Türkçe olarak özetlenmiş halidir. Kitabı okumaya ücretsiz olarak devam edebilir, destek olmak için satın alabilirsiniz. Detaylar için buraya tıklayabilirsiniz.

Kategori: Python

Yorumlar

Siz de düşüncelerinizi paylaşın

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.