Django i Behavior Driven Development na przykładzie mechanize i lettuce

Data publikacji: 2012-11-19 | Tagi:

W tym poście postaram się wyjaśnić jak zastosować pakiet lettuce wraz z mechanize do wdrożenia Behavior Driven Development w Django.

Zakładam również, że czytelnik ma podstawowe pojęcie nt. Django (tworzenie i konfiguracja projektu, tworzenie aplikacji, ogólna znajomość frameworka) oraz pythona (korzystanie z pip, ewentualnie virtualenv).

1. Tworzymy i wstępnie konfigurujemy projekt
django-admin.py startproject lettuce_example

Edytujemy podstawowe dane w settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3', 
        'NAME': 'lettuce_example.sqlite',       
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    }
}
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
)

Edytujemy podstawowe dane w urls.py

from django.conf.urls import patterns, include, url

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'lettuce_example.views.home', name='home'),
    # url(r'^lettuce_example/', include('lettuce_example.foo.urls')),

    # Uncomment the admin/doc line below to enable admin documentation:
    # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    url(r'^admin/', include(admin.site.urls)),
)
2. Instalujemy niezbędne pakiety oprogramowania
pip install mechanize
pip install lettuce
3. Tworzymy przykładową aplikację, która posłuży nam do testów
python manage.py startapp posts

Tworzymy model w aplikacji posts w pliku posts/models.py

# -*- coding: utf-8 -*-
import datetime
from django.db import models

# Create your models here.


class Post(models.Model):
    active = models.BooleanField(u'Aktywny', default=False)
    title = models.CharField(u'Tytuł', max_length=200)
    body = models.TextField(u'Treść')
    pub_date = models.DateTimeField(u'Data publikacji', default=datetime.datetime.now)

    class Meta:
        verbose_name = u'Post'
        verbose_name_plural = u'Posty'

    def __unicode__(self):
        return self.title

Dodatkowo przypisujemy tak stworzony model do panelu administracyjnego w pliku posts/admin.py

# -*- coding: utf-8 -*-
from django.contrib import admin
from .models import Post


admin.site.register(Post)

Dodajemy aplikację posts do projektu w pliku settings.py

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'posts',
)

Na koniec wystarczy zsynchronizować bazę, przy okazji tworząc nowego administratora. Dla testów przyjmijmy, że będzie miał login: admin oraz hasło: admin.

python manage.py syncdb
4. Dodajemy lettuce i mechanize do projektu

Przy okazji - wszystkie niezbędne informacje na temat lettuce można znaleźć na stronie domowej projektu. Natomiast sporo informacji na temat mechanize jest w przeróżnych cheatsheetach. Pozwolę sobie nie przytaczać linków do dokumentacji Django i Pythona :).

Najpierw łączymy lettuce z Django, edytując plik settings.py

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'posts',
    'lettuce.django',
)

Zmieniamy również nieco definicję baz danych, również w pliku settings.py

import sys

if "harvest" in sys.argv:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': 'test_lettuce_example.sqlite',
            'USER': '',
            'PASSWORD': '',
            'HOST': '',
            'PORT': '',
        }
    }
    LETTUCE_SERVER_PORT = 7000
    REMOVE_TEST_DB_AFTER_HARVEST = True
else:
    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.sqlite3',
            'NAME': 'lettuce_example.sqlite',
            'USER': '',
            'PASSWORD': '',
            'HOST': '',
            'PORT': '',
        }
    }

UWAGA!!!

Dla obeznanych z tematem testowania w Django może się to wydać dziwną konstrukcją - przyznaję, że dla mnie też to wygląda dziwnie. Ale lettuce nie wnika samo z siebie w Django dlatego sami musimy zatroszczyć się o używanie innej bazy danych przy uruchamianiu testów. Stąd ten warunek - komenda harvest odpowiada za uruchomienie testów lettuce. Dla niej podmieniamy bazę danych i ustawiamy port serwera testowego na 7000 (na porcie 8000 będzie zwykle działał serwer developerski Django).

Dodatkowo ustawiamy opcję REMOVE_TEST_DB_AFTER_HARVEST (domyślnie ma wartość True), która odpowiada za usunięcie lub nie bazy do testów - pozostawienie takiej bazy jest przydatne w przypadkach, gdy aplikacja z jakiegoś powodu nie przechodzi testów, a my nie wiemy czemu. Wtedy wystarczy ustawić tą opcję na False, uruchomić testy, a następnie odpalić cały projekt z tą testową bazą.

5. Przystępujemy do budowy testów

Najpierw tworzymy jedną niezbędną fixturkę, która będzie zawierać dane naszego testowego administratora.

python manage.py dumpdata auth.User > auth.json
mkdir posts/fixtures
mv auth.json posts/fixtures

W katalogu głównym projektu tworzymy plik terrain.py i uzupełniamy go następująco:

# -*- coding: utf-8 -*-
from lettuce import world, before, after
from django.test.utils import setup_test_environment, teardown_test_environment
from django.core.management import call_command
from django.db import connection
from django.conf import settings
import mechanize


@before.all
def init_browser():
    world.browser = mechanize.Browser()


@before.harvest
def initial_setup(server):
    print "Creating test environment..."
    call_command('syncdb', interactive=False, verbosity=0)
    call_command('loaddata', 'auth.json', verbosity=0)
    setup_test_environment()


@after.harvest
def cleanup(server):
    remove_database = getattr(settings, 'REMOVE_TEST_DB_AFTER_HARVEST', True)
    if remove_database:
        connection.creation.destroy_test_db(settings.DATABASES['default']['NAME'])
    teardown_test_environment()

W tym momencie możemy po raz pierwszy odpalić testy lettuce dla naszej aplikacji posts:

python manage.py harvest --debug-mode --apps=posts
Preparing to serve django's admin site static files...
Django's builtin server is running at 0.0.0.0:7000
Creating test environment...
Oops!
could not find features at ./posts/features
Destroying test database for alias 'default'...

Komunikat mówi nam, że nie stworzyliśmy żadnych features. A zatem czas je stworzyć.

Tworzymy najpierw katalog features w aplikacji.

mkdir posts/features

Następnie edytujemy plik posts/features/add_post.feature.

Feature: Posts

    Scenario: Add Post
        Given I access the url "/admin/"
        And I fill in "username" with "admin"
        And I fill in "password" with "admin"
        And I submit form
        Then I see the text "Witaj"

        Given I access the url "/admin/posts/post/add/"
        Then I see the text "Dodaj Post"
        And I fill in "title" with "Testowy post"
        And I fill in "body" with "Tresc testowego posta"
        And I submit form
        Then I see the text "Post "Testowy post" dodany pomyślnie."

        Then I remove all posts titled "Testowy post"

W tym momencie jeśli uruchomimy testy, dostaniemy informację, że wszystkie te kroki są niezdefiniowane. A zatem przystąpmy do ich definicji - nic prostszego.

Edytujemy plik posts/features/index.py i umieszczamy w nim następującą treść:

# -*- coding: utf-8 -*-
from lettuce import world, step
from lettuce.django import django_url
from posts.models import Post


@step(r'I access the url "(.*)"')
def access_url(step, url):
    world.response = world.browser.open(django_url(url))


@step(u'I fill in "(.*)" with "(.*)"')
def fill_in(step, field, value):
    world.browser.select_form(nr=0)
    world.browser.form[field] = value


@step(u'I submit form')
def submit_form(step):
    world.browser.submit()


@step(u'I see the text "(.*)"')
def see_text(step, text):
    response = world.browser.response().read()
    world.browser.response().seek(0)
    assert text in response.decode('utf-8')


@step(r'I remove all posts titled "(.*)"')
def remove_all_posts_titled(step, title):
    posts = Post.objects.filter(title=title)
    posts.delete()


@step(r'I logout admin')
def logout_admin(step):
    world.browser.open(django_url("/admin/logout/"))
6. Uruchamiamy testy
python manage.py harvest --debug-mode --apps=posts

Jeśli wszystko zadziałało tak jak powinno, to powinniśmy otrzymać wynik:

1 feature (1 passed)
1 scenario (1 passed)
12 steps (12 passed)

Zatem dodawanie postów działa jak należy. Teraz można napisać kolejny scenariusz obejmujący kroki zalogowania do panelu admina, dodanie nowego posta, a następnie odwiedzenie frontendowej części serwisu i sprawdzenie, czy ten post się ukazał. Taki scenariusz oczywiście zakończy się niepowodzeniem (braki w urls.py, views.py itp. itd.), więc musimy zająć się napisaniem odpowiedniej części serwisu, która pozwoli nam przejść testy bezbłędnie.

7. Zakończenie

Testowanie z lettuce wydaje się być trochę bardziej skomplikowane niż zwykłe testy Django, szczególnie jeśli chodzi o konfigurację, ale zapewniam, że taką konfigurację robi się raz na serwis, a potem to już czysta przyjemność pisania scenariuszy testujących i ewentualne dopisywanie kolejnych kroków w pliku index.py. Korzyści płynące ze stosowania testów są bardzo duże, dlatego warto się zastanowić nad ich wprowadzeniem na stałe do procesu rozwoju aplikacji.

Gotowy kod przykładu użycia lettuce z Django (w wersji 1.4.2) można pobrać z bitbucketa pod adresem https://bitbucket.org/pstankiewicz/lettuce_example.


Oceń ten post:
Podziel się:

comments powered by Disqus

IT w obrazkach: