...

пятница, 18 апреля 2014 г.

BDD-разработка на django

Программисты очень по разному относятся к тестированию, и многие не любят писать тесты. Процесс TDD же для новичков не особенно понятен — ведь приходится вместо функционала программы писать вначале тест, который его проверяет, то есть количество работы увеличивается. Однако со временем приходит осознание того, что автоматическое тестирование необходимо. К примеру, возьмем процесс разработки даже несложного проекта на django, пока в проекте пара вьюх и моделек все просто. Когда приложение обрастает функциями, внезапно обнаруживается, что совершать такое тестирование все сложнее — кликов больше, надо вносить какие-то данные и т.д., вот тут-то и на помощь приходит behavior-driven development (BDD).

image


Я хочу рассказать о BDD на примере создания примитивного приложения — рейтинга сайтов. Идея тривиальна — на странице отображается список сайтов, пользователь голосует за сайт, сайт поднимается в рейтинге и соответственно изменяет положение на странице.


Для начала в рабочей папке проекта создаем requirements.txt, с примерно таким содержанием:



Django
git+git://github.com/svfat/django-behave
splinter


Обратите внимание, в разработке я использую свой форк django-behave. Код из официального репозитария отказался работать, видимо по причине несовместимости с текущими версиями программ.



$ pip install -r requirements.txt
$ django-admin.py startproject habratest
$ cd habratest/
$ ./manage.py startapp vote


По умолчанию должна установиться последняя стабильная версия Django, и для начала разработки нам потребуется добавить лишь несколько строчек в settings.py:



INSTALLED_APPS = (
...
'vote',
'django_behave',
)
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates/'), # не забываем создать папку habratest/templates
)
TEST_RUNNER = 'django_behave.runner.DjangoBehaveTestSuiteRunner'


Первым этапом добьемся отображения списка сайтов. Для разработки в BDD-стиле с помощью имеющихся у нас инструментов, создаем папки habratest/vote/features и habratest/vote/features/steps


Здесь мы будем описывать поведение, которого мы хотим добиться от приложения. В папке features создаем файл habra.features с таким содержимым:



Feature: Habrarating

Scenario: Show a rating
Given I am a visitor
When I visit url "http://localhost:8081/"
Then I should see link contents url "habrahabr.ru"


Не очень похоже на компьютерный язык, да? Это — Gherkin. На нем можно описать поведение программы не вдаваясь в реализацию. Таким образом, писать тестовые задания может человек мало знакомый с программированием.


Мы указываем такой URL, потому что django-behave запускает тестовый сервер на порте 8081.


В той же папке создаем environment.py, код в котором выполняется до и после тестирования, и пока только обеспечивает работоспособность тестового браузера:



from splinter.browser import Browser

def before_all(context):
context.browser = Browser()

def after_all(context):
context.browser.quit()
context.browser = None


Запускаем



$ ./manage.py test vote


Ничего не получилось — тестовое окружение не понимает, что делать с шагами в файле habra.features.Видите строки желтого (ну или коричнево-желтого) цвета? Смело копируйте их в habratest/vote/features/steps/habra.feature.py, в нем описывается реализация шагов, и его содержание должно стать примерно таким:



from behave import given, when, then

@then(u'I should see link contents url "{content}"')
def i_should_see_link_contents_url(context, content):
msg = context.browser.find_link_by_partial_href(content).first
assert msg

@when(r'I visit url "{url}"')
def i_visit_url(context, url):
br = context.browser
br.visit(url)

@given(u'I am a visitor')
def i_am_a_visitor(context):
pass


Запускаем еще раз



$ ./manage.py test vote


Так, теперь мы должны видеть что наш тест фейлится, и причина очевидна — мы еще не написали ни строчки кода функционала приложения, да и данных в базе нет.


Создаем модельку в vote/models.py



class VoteItem(models.Model):
url = models.URLField()
rating = models.IntegerField(default=0)

class Meta:
# это для того чтобы в ListView наши сайты сортировались по рейтингу
ordering = ["-rating"]

def __unicode__(self):
return self.url


Делаем:



$ ./manage.py syncdb


В habratest/urls.py импортируем



from vote.views import VoteListView


и добавляем в urlpatterns



url(r'^$', VoteListView.as_view(), name="index"),


в vote/views.py



from django.views.generic import ListView
from models import VoteItem

class VoteListView(ListView):
model=VoteItem
template_name="list.html"



в habratest/templates/list.html наш шаблон в стиле ретро:



<!DOCTYPE html>
<html>
<head>
<title>Habra rating</title>
</head>
<body>
<ol>
{% for voteitem in object_list %}
<li id="{{voteitem.pk}}"><a href="{{ voteitem.url }}">{{ voteitem.url }}</a>
| Rating:{{voteitem.rating}}</li>
{% endfor %}
</ul>
</body>
</html>


При запуске тестов в памяти каждый раз создается новая БД, а после окончания удаляется, поэтому ее нам нужно заполнить какими-то данными. Для это в файл habratest/habratest/populate.py пишем:



from vote.models import VoteItem

VoteItem(url="http://www.yandex.ru", rating=6).save()
VoteItem(url="http://www.google.com", rating=5).save()
VoteItem(url="http://www.habrahabr.ru", rating=6).save()


и дописываем импорт этого скрипта в environment.py



from habratest import populate


Теперь environment.py кроме обеспечения работы браузера еще и занимается тестовой базой.


Снова запускаем



$ ./manage.py test vote


— отлично, тест пройден.Но как же рейтинг — нам надо ведь как-то голосовать за сайты


Добавляем новый сценарий в habra.feature:



Scenario: Vote for a site
Given I am a visitor
When I visit url "http://localhost:8081/"
When I click link contents "+"
Then I should see "Vote successful" somewhere in page


Тестовое окружение не знает как выполнить два последних шага, поэтому дописываем их в steps/habra.feature.py:



@then(u'I should see "{text}" somewhere in page')
def i_should_see_text_somwhere_in_page(context, text):
assert text in context.browser.html

@when(u'I click link contents "{text}"')
def i_click_link_contents_text(context, text):
link = context.browser.find_link_by_text(text).first
assert link
link.click()


После чего опять запускаем тесты. Ошибка на шаге When I click link contents "+". Так — никакой ссылки "+" у нас в шаблоне нет, как и не предусмотрена реакция на нее, что мы и исправим следующим образом (не обращайте внимание, что код никак не защищен от накрутки, это всего лишь иллюстрация):


В habratest/templates/list.html добавляем «плюс»:



<li id="{{voteitem.pk}}"><a href="{{ voteitem.url }}">{{ voteitem.url }}</a>
| Rating:{{voteitem.rating}}
| <a href={% url 'addvote' voteitem.pk %}>+<a></li>


Соответственно создаем примитивную вьюшку для addvote в vote/views.py:



from django.shortcuts import render_to_response

def addvote(request, pk):
return render_to_response('successful.html', context)


добавляем ее в urls.py,



from vote.views import VoteListView, addvote


и



url(r'^plus/(?P<pk>\d+)/$', addvote, name='addvote'),


И шаблон templates/successful.html:



<!DOCTYPE html>
<html>
<head>
<title>Habra rating</title>
</head>
<body>
<p>Vote successful</p>
</body>
</html>


Запускаем тесты — все должно пройти успешно.


А теперь напишем тест для проверки работоспособности увеличения рейтинга. Мы должны увидеть изменения в списке при голосовании, здесь воспользуемся тем, что знаем исходные данные (которые внесли в populate.py), так как y habrahabr.ru rating=6, при клике на "+" его рейтинг должен измениться на единицу, и по выполнению предыдущего сценария, его рейтинг должен стать равным «7».



Scenario: Vote for a site and look at the rating
Given I am a visitor
When I visit url "http://localhost:8081/"
Then I should see "Rating:7" somewhere in page


Опять последний шаг не выполняется. Для того, чтобы исправить это, дописываем вьюшку addvote:



def addvote(request, pk):
item = VoteItem.objects.get(pk=pk)
item.rating += 1
item.save()
return render_to_response('successful.html')


Проверяем, теперь тест проходит успешно.


Итак, продолжая писать тесты на gherkin (что, как я уже говорил выше, может делать даже человек не знакомый с программированием) мы фактически создадим часть технического задания и, одновременно, приемочного тестирования нашего приложения.


Я написал эту статью, так как на русском языке практически нет информации по BDD с django, и, если ее вдруг будут читать специалисты в этом вопросе, не поленитесь, напишите в комментариях о своей практике. Это будет полезно всем. С радостью приму ваши замечания, исправления и критику. Спасибо!


Что еще почитать:

Behave documentation

Splinter — test framework for web applications

Django Full Stack Testing and BDD with Lettuce and Splinter


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий