Django BDD Development

  • Tutorial
Programmers have a very different attitude to testing, and many do not like to write tests. The TDD process is not particularly clear for beginners - after all, instead of the program’s functionality, you first have to write a test that checks it, that is, the amount of work increases. However, over time comes the realization that automatic testing is necessary. For example, let's take the development process of even a simple django project, while a couple of views and models are simple in the project. When the application is overloaded with functions, it suddenly turns out that it’s more difficult to perform such testing - there are more clicks, you need to enter some data, etc., this is where behavior-driven development (BDD) comes to the rescue.


I want to talk about BDD as an example of creating a primitive application - ranking sites. The idea is trivial - the page displays a list of sites, the user votes for the site, the site rises in the ranking and accordingly changes the position on the page.

First, create requirements.txt in the project’s working folder, with approximately the following content:

Please note that in development I use my fork django-behave. The code from the official repository refused to work, apparently due to incompatibility with current versions of programs.
$ pip install -r requirements.txt
$ startproject habratest
$ cd habratest/
$ ./ startapp vote

By default, the latest stable version of Django should be installed, and to start development, we need to add only a few lines to
    os.path.join(BASE_DIR, 'templates/'), # не забываем создать папку habratest/templates
TEST_RUNNER = 'django_behave.runner.DjangoBehaveTestSuiteRunner'

The first step is to display a list of sites. To develop in BDD style using the tools we have, create the folders habratest / vote / features and habratest / vote / features / steps

Here we will describe the behavior that we want to achieve from the application. In the features folder, create a habra.features file with the following contents:
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 ""

Not really like a computer language, right? This is Gherkin. On it, you can describe the behavior of the program without going into implementation. Thus, a person familiar with programming can write test tasks.

We specify this URL because django-behave starts the test server on port 8081.

In the same folder, create, the code in which is executed before and after testing, and so far only ensures the test browser is working:
from splinter.browser import Browser
def before_all(context):
    context.browser = Browser()
def after_all(context):
    context.browser = None

We launch
$ ./ test vote

Nothing happened - the test environment does not understand what to do with the steps in the habra.features file. Do you see lines of yellow (well, or brown-yellow) color? Feel free to copy them to habratest / vote / features / steps /, it describes the implementation of the steps, and its content should look something like this:
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
@given(u'I am a visitor')
def i_am_a_visitor(context):

Run again
 $ ./ test vote

So, now we need to see that our test fails, and the reason is obvious - we have not written a single line of application functional code, and there is no data in the database.

Create a model in vote /
class VoteItem(models.Model):
    url = models.URLField()
    rating = models.IntegerField(default=0)
    class Meta:
        # это для того чтобы в ListView наши сайты сортировались по рейтингу
        ordering = ["-rating"]
    def __unicode__(self):
        return self.url

We do:
$ ./ syncdb

In habratest / we import
from vote.views import VoteListView

and add to urlpatterns
    url(r'^$', VoteListView.as_view(), name="index"), 

in vote /
from django.views.generic import ListView
from models import VoteItem
class VoteListView(ListView):

in habratest / templates / list.html our retro style template:
Habra rating
    {% for voteitem in object_list %}
  1. {{ voteitem.url }} | Rating:{{voteitem.rating}}
  2. {% endfor %}

When tests are run, a new database is created in memory each time, and after the end it is deleted, so we need to fill it with some data. For this, in the file habratest / habratest / write:
from vote.models import VoteItem
VoteItem(url="", rating=6).save()
VoteItem(url="", rating=5).save()
VoteItem(url="", rating=6).save()

and add the import of this script to
from habratest import populate

Now,, in addition to ensuring the operation of the browser, is also engaged in a test base.

We start again
$ ./ test vote 

- excellent, the test passed. But what about the rating - we need to somehow vote for the sites.

Add a new script to 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

The test environment does not know how to complete the last two steps, so we add them to steps /
@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

Then we run the tests again. The error in step When I click link contents "+". So - we don’t have a “+” link in the template, just as there is no reaction to it, which we will correct as follows (do not pay attention that the code is not protected from cheating in any way, this is just an illustration):

In habratest / templates /list.html add a plus:
  • {{ voteitem.url }} | Rating:{{voteitem.rating}} | +

  • Accordingly, we create a primitive view for addvote in vote /
    from django.shortcuts import render_to_response
    def addvote(request, pk):
        return render_to_response('successful.html', context)

    add it to,
    from vote.views import VoteListView, addvote

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

    And the template templates / successful.html:
    Habra rating

    Vote successful

    Run the tests - everything should succeed.

    And now we will write a test to test the performance of increasing the rating. We should see the changes in the list during the voting, here we will take advantage of the fact that we know the source data (which were made in, since y rating = 6, when clicking on “+” its rating should change by one, and according to the previous scenario, its rating should be equal to "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

    Again, the last step is not performed. In order to fix this, we add the addvote view:
    def addvote(request, pk):
        item = VoteItem.objects.get(pk=pk)
        item.rating += 1
        return render_to_response('successful.html')

    We check, now the test is successful.

    So, continuing to write tests on gherkin (which, as I said above, even a person who is not familiar with programming can do), we will actually create part of the technical task and, at the same time, acceptance testing of our application.

    I wrote this article, since in Russian there is practically no information on BDD with django, and if experts in this matter suddenly read it, do not be lazy, write in the comments about your practice. It will be useful to everyone. I will gladly accept your comments, corrections and criticism. Thanks!

    What else to read:
    Behave documentation
    Splinter - test framework for web applications
    Django Full Stack Testing and BDD with Lettuce and Splinter

    Also popular now: