Пользователь
0,0
рейтинг
18 апреля 2014 в 12:42

Разработка → BDD-разработка на django tutorial

Программисты очень по разному относятся к тестированию, и многие не любят писать тесты. Процесс 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
Станислав Фатеев @svfat
карма
24,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (41)

  • +3
    Круто! Django творит чудеса))
  • +2
    На всякий случай залил код на github. Просто запускайте
    $ git clone git@github.com:svfat/habratest-bdd
    
    чтобы получить копию
    • 0
      Спасибо посмотрю
  • –1
    Думал очередная, наивная статья: «как начать писать тесты» — ан нет!
  • 0
    Проект django-behave не поддерживается в данные момент, ищется мейнтейнер. Поддержки python 3 нет и не предвидится.
    • 0
      Это так. Я написал в начале о проблемах несовместимости. В своем форке я корявенько пофиксил их. Что-то посоветуете?
  • 0
    When I visit url "http://localhost:8081/"

    А как быть с тестовыми доменами? Или с продакшеном?
    • 0
      Уточните, не понимаю в чем проблема? Просто определяете этот шаг в steps.py как вам угодно, подтягиваете туда любые домены
      • 0
        Есть динамически генерируемые тестовые домены для каждой feature-ветки, есть просто альтернативные домены для сайта, и прочее. Как проверить текущий домен?
        Более того, такие тесты не учитывают изменения в urls.py, и хотя тестируемые вьюхи не меняются — в тесте (насколько я понимаю) адрес прописан явно. Как это обойти?
        • 0
          По второму вопросу: ведь адрес можно доставать из urls.py с помощью django.core.urlresolvers.reverse. Первый вопрос, я, если честно до конца не понял, но думаю можно генерировать имена доменов аналогично тому, как генерируются они в вашем приложении.
          • 0
            Первый вопрос заключается в том, что мне непонятно как прописать в сценарии относительные пути. Второй вопрос — можно ли в сценарии направлять не напрямую на URL, а на вьюху?
    • 0
      I don't always test my code But When I do, I do It in Production?

      Вроде бы BDD (как и родные джанговские тесты) реальные домены использовать не должно никоим образом.
      • 0
        Я не тестирую на продакшене, я имитирую боевые условия, при которых для разных (под)доменах результат для вьюх — разный.
        • +1
          А руками вы как проверяете эти фичи? Обычно делаются поддомены для localhost или каким-то GET-параметром/кукой мокается домен, но это можно и в тестовый сценарий записать без проблем.
          • 0
            Руками я просто создаю поддомен на тестовом серваке с именем ветки, добавляю ее в Sites, и тестирую.
          • 0
            Более того, я считаю тестирование на локалхосте — ужасной ошибкой, поскольку есть разница в окружении между ним и боевым сервером (например разные версии системных библиотек и/или приложений, таких как libjpeg, например). Проще клонировать боевой сервер, и разворачивать тестирование на нем с использованием боевых данных (но закрыв, например, доступ к отправке почты).
  • +4
    Еретический метод — внесение абсолютно лишнего уровня абстракции.

    Полностью «классические» тесты заменить на него не получится, а значит его использование в проекте только увеличит сложность поддержки (через добавление лишних сущностей и связей), не принося существенной пользы.
    • 0
      Что значит «заменить классические тесты»? Мне кажется, все сильно зависит от цели тестирования.
      • +1
        Придется обучать тестеров (или кто там у вас пишет тесты) этому псевдо-языку. Не проще ли использовать привычный python, раз уже они на нем пишут? Полностью согласен — мне не ясны цели создания этого слоя.
        • 0
          Юнит-тесты (для которых Gherkin не годится) пишут все же не тестеры, зато Gherkin есть и для Руби, например. То есть, тестер сможет писать тесты для приложений хоть на Питоне, хоть на Руби, хоть еще на чем. По-моему, это даже не язык программирования, а что-то половинчатое, на уровне недо-блок-схемы, т.е. тестер может вообще не быть программистом, более того, в таком виде записать требования может хоть ПМ (а тест из требований получается сам собой).

          Во многих случаях ваш подход тоже верен, но часто если можно попробовать упростить коллегам жизнь — почему бы этого не сделать.
          • +1
            Пока не появилось интерпретатора, который бы мог как-то обрабатывать простую человеческую речь, язык программирования будет языком программирования, не знание конструкций которого будет приводить к ошибкам, падению и воплям:

            — Лёх, а чо у меня тут, позырь, пожалуйста
            — Сам позырь документацию!

            Так что формальный он или какой еще, разница только в сложности языков. Впрочем, не знаю… может быть я и не прав в том, что воспринимаю вштыки подобные вещи.
            • 0
              К сожалению, простую человеческую речь часто можно понять неоднозначно, даже у человеков такая проблема, не говоря уже о железяках. Сейчас у компьютера есть большой плюс — он практически (по сравнению с человеком) не ошибается.
            • 0
              Особую печаль у того, кто будет смотреть в документацию, должен вызвать тот факт, что документации то никакой нет, мы просто привязываем какую-то фразу к питон коду (ещё не факт, что фраза эта будет грамотной и понятной). Возможно, конечно, в реальных проектах есть люди, которые умудряются кроме тестов писать ещё и исчерпывающую документацию как для пользователей, так и для разработчиков, но в основном наверняка нет.
              Поэтому будет ещё круче:
              — Лёх, а чо у меня тут, позырь, пожалуйста
              — Ну открой сорцы, да глянь!
      • +2
        Глобальная цель у тестирования одна — убедиться что поведение проекта соответствует спецификации (как бы она ни была выражена).

        Использование BDD предполагает:

        1. Введение в проект нового ЯП со своим синтаксисом и семантикой (а значит обучение им всех специалистов, работающих с тестами).
        2. Дублирование части функционала, для возможности его использования в BDD.
        3. Поддержка дублированного функционала (как только будет надо сделать какое-то новое действие, его надо замапить в правила).

        Человек, который пишет тесты (а значит явно описывает требования спецификации ПО), должен быть в состоянии писать простейшие алгоритмы. Если он не может этого делать, то его нельзя допускать до этой работы. А если он может писать алгоритмы, то разницы между:

        — user.create(name='test_name')
        — «create user with name 'test_name'»

        никакой нет, за тем исключением, что программистам будет проще разобрать 1-ый вариант, так как они его по 20 раз на дню видят.
        • 0
          «Новый ЯП» это некоторый полу-формальный формат записи спецификаций, не более. Он не идеален, и если есть возможность, лучше спецификацию иметь отдельно от тестов, но возможность есть не всегда. Такая «спецификация-тест» для человека со стороны лучше, чем простой тест и никакой спецификации.
          • +1
            >«Новый ЯП» это некоторый полу-формальный формат записи спецификаций, не более.
            И тем не менее ему надо учить всех людей, которые будут с ним взаимодействовать и большинство из которых точно может обойтись без него. Зачем тогда его вводить? Этот полу-формальный формат — отличный пример лишней сущности.

            >Такая «спецификация-тест» для человека со стороны лучше, чем простой тест и никакой спецификации.
            Простой тест и есть спецификация, причём максимально конкретная. Более того, простой тест более гибок, чем правила на специфичном для проекта ЯП, описывающем только часть (причём меньшую) предметной области.

            Даже писать простые тесты с подробными комментариями на нормальном языке проще, чем поддерживать отдельный ЯП для описания этих тестов на «почти» нормальном языке.

            Вообще, кто этот мифический «человек со стороны»?
            • 0
              Не-программист, не-разработчик. ПМ, например. Для него Питон это какие-то непонятные закорючки и скобочки, если прочесть их как-то можно, то написать практически нереально.

              Зайдя на страницу товара 123 мы должны увидеть title

              намного проще написать и понять, не будучи программистом чем

              result = self.client.get('/products/123/') self.assert('title' in result.body)
              • +1
                Буду рад увидеть здесь комментарий человека, который не понимает что написано в этих строках (и знает английский):

                > result = self.client.get('/products/123/')
                > self.assert('title' in result.body)

                Человек, который работал в Excel, писал макросы для Word, настраивал что-нибудь в Jira, или ещё чего-нибудь похожее делал хоть раз в своей жизни, поймёт что тут написано. Это не говоря уже о том, что не ясно зачем ПМ-у вообще лезть в эти тесты.

                Если же по какой-то неимоверной случайности этот ПМ вообще не сталкивался с программированием, то вот хороший повод узнать таки, чем он управляет.
                • 0
                  Погодите, я же объяснял выше — иногда, в некоторых командах, на некоторых проектах, надо не только прочесть, но и написать тоже. И удобно, когда сразу пишется и спецификация, и тест — а Gherkin гораздо больше похож на нормальную спецификацию, чем перегруженный служебными символами питоний код, вот и все. Написать текст с пятком ключевых слов проще, чем рабочий код на питоне, если изначально не знаешь ни того, ни другого.
                  • 0
                    Если изначально не знаешь ни python, ни gherkin, как думаете, что будет в итоге полезнее — поупражняться в python или в псевдо-языке с очень узкой нишей и невнятным будущим?
                    • 0
                      Полезнее сделать максимальный объем работы максимально эффективно. Почитайте про то, откуда вообще появился BDD, он решает вполне конкретные проблемы TDD. Проблемы не то чтобы критичные, но если все будут думать по-вашему, движения не будет ни в какую сторону.

                      Есть прекрасные машинные коды, пускай все, кто хочет быть программистом, первым делом в них разбираются, иначе какие же они программисты? А как разберутся — пускай на них и пишут, зачем придумывать какие-то еще «языки высокого уровня».

                      Еще раз повторюсь: если вам не нужен BDD, это не значит, что он вообще никому не нужен.
                    • 0
                      Это ведь тот же cucumber, который очень распространен в среде rails habrahabr.ru/post/62958/
              • 0
                А что этот человек в проекте-то делает?)
                • 0
                  > ПМ, например
                  • 0
                    Гнать в шею ;)
                    • 0
                      Но в любом случае, наличие читабельной спецификации лучше чем ее отсутствие, правда? Я тут на досуге почитал всякие мысли про BDD — они еще правильно акцентируют внимание на том, что, собственно, тестируется. «Обычные» тесты на «обычном» ЯП подталкивают к юнит-тестированию, тестированию реализации, когда BDD «настраивает» на тестирование других вещей другими методами. В общем, реально разница некритичная, и в принципе можно всего того же добиться классическим TDD, но психологически сложнее. Программисты — ленивые сволочи в массе своей, и напрягаться не любят :)
                      • 0
                        Именно поэтому нам и платят — за автоматизацию ;)
    • 0
      Думаете, что увеличится сложность поддержки? Некоторые считают что чем больше тестов, тем проще поддерживать, так как все работает правильно. Меня очень удивило покрытие тестами кода sqlite.
      As of version 3.8.0, the SQLite library consists of approximately 84.3 KSLOC of C code. (KSLOC means thousands of «Source Lines Of Code» or, in other words, lines of code excluding blank lines and comments.) By comparison, the project has 1084 times as much test code and test scripts — 91452.5 KSLOC.
      • +2
        Наворачивание ещё одного уровня абстракции (которым является BDD) над тестами увеличит сложность поддержки тестов.
        Я ни в коем случае не говорил, что сами тесты — плохо, тесты — это хорошо, но только до тех пор, пока над ними не начинают ставить странные опыты.
        • +1
          Точка входа действительно будет выше, однако, практики тестирования везде разные. В некоторых компаниях BDD является стандартом, в этом случае нет никакого дополнительного уровня абстракции, т.к. тесты просто будут писаться в другом стиле. Однажды привыкнув команда будет использовать этот стиль вполне естественно, хотя новичку в BDD будет так же сложно как программисту С начать писать на С++.

          На счёт главного Вашего аргумента Gherkin — "лишний язык" в python проекте, мне кажется что вы погорячились… Так или иначе при командной разработке мы вынуждены договариваться между собой и заключать соглашения о том как делать хорошо а как плохо. Без подобных соглашений может стать мучительно больно вносить какие-либо модификации в долго живущий проект. Велика ли разница между "языком спецификаций" Gherkin и соглашением писать документацию по функциям и классам в doxygen-стиле?

          Я не поборник BDD, просто главный аргумент "долой другие языки из проекта" мне кажется некорректным. Так или иначе в вебе мы вынуждены знать какой-то стек технологий, выбор стека дело вкуса каждой конкретной команды
          • +1
            В некоторых компаниях BDD является стандартом, в этом случае нет никакого дополнительного уровня абстракции, т.к. тесты просто будут писаться в другом стиле
            Согласен, что такое может быть, но примеров не знаю совсем. Очень хочу посмотреть на чистые BDD тесты в большом проекте.

            На счёт главного Вашего аргумента Gherkin — "лишний язык" в python проекте, мне кажется что вы погорячились…
            Я не вообще против использования других языков, я против использования их конкретно в этом случае. Считаю, что язык эффективно занимается хорошим api.

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