Ö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;
- FT yazarak başlıyoruz, kullanıcının gözünden uygulamanın yeni bir işlevini tanımlıyoruz, açıklıyoruz.
- 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ı.
- 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.
- 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'...
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'...
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.
Yorumlar