DEV Community

Cover image for Test-Driven Development with Django
PyMeister
PyMeister

Posted on

Test-Driven Development with Django

Initializing the Test Project

Open your terminal and navigate to the directory that contains your Django projects. On my computer, that folder is called Projects.

cd Projects
Enter fullscreen mode Exit fullscreen mode

Create a new Django project, test-driven, using the django-admin.py utility, and change directories to the newly generated folder.

django-admin.py startproject test-driven && cd test-driven
Enter fullscreen mode Exit fullscreen mode

Create a new virtual environment using the virtualenvwrapper utility.

mkvirtualenv testdriven
Enter fullscreen mode Exit fullscreen mode

or Create a new virtual environment using the venv utility.

python -m venv env 
Enter fullscreen mode Exit fullscreen mode

Start PyCharm and click File > Open in the menu bar. Find and select the test-driven project folder. When prompted, choose to open the project in a new window. Wait for the project to open and then click PyCharm > Preferences > Project Interpreter in the menu bar to open the Project Interpreter panel. Click the gear icon in the upper right corner of the panel; then, select the "Add local..." option. In file explorer, navigate to your virtual environments folder, find the test-driven directory, and select the python2.7 file in the bin folder. On my machine, this file is located at ~/Virtualenvs/test-driven/bin. In PyCharm, close the Preferences window. The virtual environment you just added is now being used by PyCharm for the test-driven project.

Now that we have installed our virtual environment, we must add some dependencies to our project. First, install Django.

pip install django
Enter fullscreen mode Exit fullscreen mode

Return to PyCharm and run the Django project by selecting Run from the menu bar. PyCharm should start the development server and log the progress in a terminal window. Click the link that PyCharm provides and your browser should open to a default Django page that reads "It worked!"

Next, install the Psycopg Python database adapter, so that we can use PostgreSQL as our database.

pip install psycopg2
Enter fullscreen mode Exit fullscreen mode

In PyCharm, open the settings.py file in the test-driven app directory. Change the DATABASES attribute to use PostgreSQL instead of SQLite.

# test-driven/settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'test-driven',
        'USER': 'admin',
        'PASSWORD': 'password1',
        'HOST': 'localhost',
        'PORT': '5432'
    }
}
Enter fullscreen mode Exit fullscreen mode

Start the pgAdmin III program, and create a new database called testdriven. In the window that appears, enter test-driven in the name field and admin in the owner field. Close the window and then find the newly created database. Notice it does not have any tables yet.

Return to PyCharm and delete the db.sqlite3 file that was created when you ran the Django project initially. Migrate the database to create the default Django tables.

python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

If you examine your database in pgAdmin III, you should see 10 new tables. At this point, we are ready to start tracking our file changes using Git. Initialize a Git repository and commit all of the file changes that we have done up to this point.

git init
git add .
git commit -m "Initial commit."
Enter fullscreen mode Exit fullscreen mode

Beginning the Test-Driven Development Process

In general, the test-driven development workflow follows these steps:

  1. Write a short story that captures what a user needs to complete a requirement.
  2. Wireframe a simple model of an application or feature that meets the requirement.
  3. Write a use case that narrates a scenario based on the wireframe.
  4. Write a functional test that follows the actions outlined in the use case.
  5. Write unit tests to evaluate the operability of low-level code.
  6. Write functional code that satisfies the tests.

These are the critical tenets of test-driven development:

  • Always write a failing test before you program any functional code.
  • Write the minimum amount of code necessary to make a test pass.
  • When a test passes, restart the process or refactor the code if necessary.

Writing a User Story

Conventionally, the user story should be short enough to be written on a notecard. It is written in the language of the client and avoids the use of technical vocabulary. Here is our user story:

John wants an application that allows him to manage a list of organizations.

Wireframing an Application

Our application will consist of two pages, a home page that lists organizations and a create page that generates new organizations. Sketch a simple layout of the application and make sure to include components and page states. See the attached document for an example of an application wireframe created with MockFlow.

Writing a Use Case

Use our wireframe to imagine how the typical user will interact with our application. Follow along with this outline of an expected user experience:

John goes to the home page. He sees a data table with a single cell that says "No organizations". He also sees a button labelled "Create organization". He clicks the create button. The page refreshes and John sees a form with a single text input control and a submit button. John enters an organization name and clicks the submit button. The page refreshes and John notices that the table now has a single row containing the details of the organization that he added.

Writing a Functional Test

A functional test (acceptance test) follows the scenario laid out in a user case. It evaluates the system from the user's point of view. Functional tests are helpful because they allow us to mimic the behavior of a real user, and they can be repeated consistently over time. Do not write exhaustive functional tests that touch every possible interaction and cover every single outcome that can occur within a system. Instead, focus on the important aspects of a user experience, and write tests that encompass the most popular and direct set of actions that a user is expected to follow. In practice, a separate quality assurance (QA) team should evaluate the application for bugs.

Leverage software like Selenium to drive the functional tests in Django. Selenium provides functionality that allows automated tests to interact with a browser in an authentic way. Examples include opening and closing browser windows, navigating to URLs, and interacting with on-screen components using mouse and keyboard events like a human user.

Implement functional tests by creating a new Python directory called functional_tests in the test-driven project folder. Open the project's settings.py file and change the INSTALLED_APPS attribute as shown below.

# test-driven/settings.py

DEFAULT_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
)

LOCAL_APPS = (
    'functional_tests',
)

INSTALLED_APPS = DEFAULT_APPS + LOCAL_APPS
Enter fullscreen mode Exit fullscreen mode

Install the Selenium package in the terminal.

pip install selenium
Enter fullscreen mode Exit fullscreen mode

Create a new Python file in the functional_tests folder and call it test_organizations. Set up a simple test case.

# functional_tests/test_organizations.py

from selenium.webdriver.firefox.webdriver import WebDriver
from django.test import LiveServerTestCase


class OrganizationsTest(LiveServerTestCase):
    def setUp(self):
        self.browser = WebDriver()
        self.browser.implicitly_wait(5)

    def tearDown(self):
        self.browser.quit()
Enter fullscreen mode Exit fullscreen mode

In Django, functional tests extend the django.test.LiveServerTestCase class. Unlike the standard TestCase, the LiveServerTestCase starts a real development server that allows an automated test client like Selenium to operate within a browser. The setUp() method is run immediately before a test starts and the tearDown() method is run directly after the test completes. When run, each test will open a web browser, execute some behaviors, and then close the browser. Run the functional tests and pay attention to the output.

python manage.py test functional_tests
Enter fullscreen mode Exit fullscreen mode

The functional tests run successfully with no failures. In fact, we have not actually programmed any tests yet. Create a functional test that follows that actions narrated by the use case. In the example below, the use case is broken into digestible instructions that guide our development.

# functional_tests/test_organizations.py

def test_new_organizations_are_shown_in_list(self):
    # John goes to the home page. 
    self.browser.get(self.live_server_url)

    # He sees a data table with a single cell that says "No organizations".
    # He also sees a button labelled 'Create organization'. He clicks the create button. 
    cell = self.browser.find_element_by_xpath('//table/tbody/tr/td')
    self.assertEqual(cell.text, 'No organizations')
    create_button = self.browser.find_element_by_id('create-button')
    self.assertEqual(create_button.text, 'Create organization')
    create_button.click()

    # The page refreshes and John sees a form with a single text input control and a submit button.
    name_input = self.browser.find_element_by_name('name')
    self.assertEqual(name_input.get_attribute('placeholder'), 'Organization name')

    # John enters an organization name and clicks the submit button. 
    name_input.send_keys('TDD Organization')
    submit_button = self.browser.find_element_by_id('submit')
    self.assertEqual(submit_button.text, 'Submit')
    submit_button.click()

    # The page refreshes and John notices that the table now has a single row containing 
    # the details of the organization that he added.
    row = self.browser.find_element_by_xpath('//table/tbody/tr')
    self.assertIn('TDD Organization', row.text)
Enter fullscreen mode Exit fullscreen mode

Our functional test is pretty simple: 14 lines of code and 5 assertions. Every test method begins with the prefix test_. Selenium provides some pretty useful methods to interact with the elements on the page. Walk through what is happening in the test:

  • The home page opens in the browser.
  • The browser searches the HTML content for a <td> element and checks to make sure it says "No organizations".
  • The browser searches the HTML content for a create button, then click its.
  • At this point, the client should navigate to a new page. When it refreshes, the browser checks the HTML content for a text input control and a submit button. It also confirms that the text field has a placeholder that says "Organization name".
  • The browser enters an organization name into the text field and clicks the submit button.
  • At this point, the client should return to the home page and the browser checks to make sure the table contains a row with the name of the organization added on the previous page.

Notice how this test combines the use case and the wireframe. Run the functional test again and see what happens.

python manage.py test functional_tests
Enter fullscreen mode Exit fullscreen mode

A web browser opens, waits 5 seconds, and then closes with a failure. This is good! In fact, tests should always initially fail. Notice that the test failed with the message "Unable to locate element". It cannot find the <td> element in the rendered HTML. You might have also observed that the web page itself could not be found when it opened in the browser. In order to address these failures we need to write our first unit test.

Writing a Unit Test

While a functional test mimics a real user interacting with an application, a unit test ensures that the functionality of the bits of code themselves work as expected. Our web application failed to load because we don't have anything to load yet. We're missing the fundamental elements of a Django web application: URLs, views, and templates.

Let's create a new Django app called organizations. We're creating a new app instead of using the default project app in reaction to the guidelines laid out by the creators of Django. The idea is that Django apps should be autonomous from the project, so that they can be packaged and distributed easily as individual modules.

python manage.py startapp organizations
Enter fullscreen mode Exit fullscreen mode

You'll notice that your Django project is updated in PyCharm. Open your project settings.py and add organizations as an installed local app. This action configures and registers the module with Django, so that the URLs, views, forms, models, etc. are accessible throughout the project.

# test-driven/settings.py

LOCAL_APPS = (
    'functional_tests',
    'organizations',
)
Enter fullscreen mode Exit fullscreen mode

Inside the organizations folder, delete the tests.py file and create a Python directory called tests. Create a new Python file called test_views and place it in the tests folder. This is where our unit tests will live. Add the code shown below to create a unit test class for our views.

# organizations/tests/test_views.py

from django.test import Client
from django.test import TestCase


class HomeViewTest(TestCase):
    def setUp(self):
        self.client = Client()
Enter fullscreen mode Exit fullscreen mode

This class creates a test case and sets up each test method to use a fake client that will interact with the framework as if it were real. Remember, this is the basic way that Django works:

  1. A web client sends an HTTP request to the server.
  2. The server passes the request to the Django web framework.
  3. Django parses the URL from the request and resolves it to an associated view method.
  4. The view processes some code that usually involves communicating with a database and then returns an HTTP response.
  5. The response typically contains a string of HTML text, which is usually rendered from a template.
  6. The web client receives the response and displays the content as a web page.

Let's test out our new unit test.

python manage.py test organizations
Enter fullscreen mode Exit fullscreen mode

Our tests are working.

Let's add some actual logic to our unit test. At this point, we can reasonably expect that a user should be shown a web page when he or she enters a URL into the browser. Thus, our first unit test is very straightforward.

# organizations/tests/test_views.py

def test_view_renders_template(self):
    response = self.client.get('/')
    self.assertTemplateUsed(response, 'organizations/home.html')
Enter fullscreen mode Exit fullscreen mode

In this test, we are simulating a call to the home page and are confirming that we are rendering the expected template. When we run the unit tests, they fail. Remember that's a good thing! Now, let's write the minimum amount of code necessary to make this test pass. The first thing we need is a template. Create a templates/organizations directory in your organizations folder. Create a simple HTML file in the new directory and name it home.html.

<!-- organizations/templates/organizations/home.html -->

<!DOCTYPE html>

<html>

<head lang="en">
    <meta charset="utf-8">
    <title>Test-Driven</title>
</head>

<body></body>

</html>
Enter fullscreen mode Exit fullscreen mode

Next, open the views.py file from the organizations folder. Add the minimum amount of code necessary to render the template.

# organizations/views.py

def home_view(request):
    return render(request, 'organizations/home.html')
Enter fullscreen mode Exit fullscreen mode

Lastly, open the urls.py file in the test-driven folder and adjust the URL configuration as shown.

# test-driven/urls.py

from django.conf.urls import patterns
from django.conf.urls import re_path

urlpatterns = patterns('organizations.views',
    re_path(r'^$', 'home_view', name='home'),
)
Enter fullscreen mode Exit fullscreen mode

Run the unit tests again. They pass! Let's try the functional tests again. They fail with the same error as before, but at least we're actually rendering the expected web page. This is a good time for a commit. Look at the status of the project and then add and commit all of our new and modified files.

git status
git add .
git commit -m "Added functional tests and organizations."
Enter fullscreen mode Exit fullscreen mode

Exploring the Test-Driven Development Process

At this point, we've gotten a taste of how the basic flow of TDD works. We've created our functional test, which is driving the entire workflow. We've created our very first unit test in order to push through our functional test failures. The process is iterative: run the functional test until it breaks, create a failing unit test, write a small amount of code to make it pass, and repeat until no more errors exist. At this point, our functional test is failing because it cannot find a table cell. Let's remedy that by adding it to the template.

<body>
    <table>
        <tbody>
            <tr>
                <td>No organizations</td>
            </tr>
        </tbody>
    </table>
</body>
Enter fullscreen mode Exit fullscreen mode

Run the functional tests. They fail again, but notice that we have a new error! We've moved a step forward. Now, the create button cannot be found. Let's return to the template.

<body>
    <a id="create-button" href="#">Create organization</a>
    <table>
        <tbody>
            <tr>
                <td>No organizations</td>
            </tr>
        </tbody>
    </table>
</body>
Enter fullscreen mode Exit fullscreen mode

Run the functional tests. We've progressed a little more! Now, the test cannot find the text input control, but if we look at the user story, we realize the test fails because the page never changes. If we look at our wireframe, we can see that we need a second page, the create page. Let's follow the same steps as when we created the home page. First, create a unit test. Notice how we use a new test case for a new view.

# organizations/tests/test_views.py

class HomeViewTest(TestCase): ...

class CreateViewTest(TestCase):
    def setUp(self):
        self.client = Client()

    def test_view_renders_template(self):
        response = self.client.get('/create/')
        self.assertTemplateUsed(response, 'organizations/create.html')
Enter fullscreen mode Exit fullscreen mode

Next, we follow the same steps for creating the URL, view, and template as we did for the home page. Create a new HTML file in the templates/organizations folder called create.html.

<!-- organizations/templates/organizations/create.html -->

<!DOCTYPE html>

<html>

<head lang="en">
    <meta charset="utf-8">
    <title>Test-Driven</title>
</head>

<body></body>

</html>
Enter fullscreen mode Exit fullscreen mode

Create the other Django files.

# organizations/views.py

def home_view(request): 

def create_view(request):
    return render(request, 'organizations/create.html')
Enter fullscreen mode Exit fullscreen mode
# test-driven/urls.py

urlpatterns = patterns('organizations.views',
    re_path(r'^$', 'home_view', name='home'),
    re_path(r'^create/$', 'create_view', name='create'),
)
Enter fullscreen mode Exit fullscreen mode

Run the unit tests. They passed! Now that we have a working web page, we can link to it in the home.html template.

<!-- organizations/templates/organizations/create.html -->

<a id="create-button" href="{% url 'create' %}">Create organization</a>
Enter fullscreen mode Exit fullscreen mode

Run the functional tests again. We can see the browser navigate to the create page, so we've passed one more hurdle. The tests fail because the text input field cannot be found. Let's add it.

<!-- organizations/templates/organizations/create.html -->

<input type="text" name="name" placeholder="Organization name">
Enter fullscreen mode Exit fullscreen mode

Remember, we just want the minimum amount of code necessary to move forward in the test. Don't get ahead of yourself! The functional tests fail, but we've progressed one more step. We need the submit button.

<!-- organizations/templates/organizations/create.html -->

<button id="submit">Submit</button>
Enter fullscreen mode Exit fullscreen mode

We've taken another step! Again the test is failing because it cannot find an element, but we know that the real reason is because the page hasn't navigated back home yet. Before we do anything else, let's commit our changes.

git add .
git commit -m "Home and create pages rendering correctly."
Enter fullscreen mode Exit fullscreen mode

We need our application to return to the home page after a post request. Let's create a new unit test.

# organizations/tests/test_views.py

class CreateViewTest(TestCase):
    ...

    def test_view_redirects_home_on_post(self):
        response = self.client.post('/create/')
        self.assertRedirects(response, '/')
Enter fullscreen mode Exit fullscreen mode

The unit test fails, so let's make it pass.

# organizations/views.py

from django.core.urlresolvers import reverse
from django.shortcuts import redirect

...

def create_view(request):
    if request.method == 'POST':
        return redirect(reverse('home'))

    return render(request, 'organizations/create.html')
Enter fullscreen mode Exit fullscreen mode

The unit test passes. The functional tests are still failing because we haven't actually added any posting behavior to our controls yet. Let's upgrade our template, so that it actually uses a form. Remember that all post calls need a CSRF token.

<!-- organizations/templates/organizations/create.html -->

<form action="{% url 'create' %}" method="post">
    {% csrf_token %}
    <input type="text" name="name" placeholder="Organization name">
    <button id="submit" type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

That gets the functional test moving ahead to the next failure. The new organization we created should be displayed in the list, but its not. It's time for some more advanced testing. We need to employ the use of models if we want to save our organizations. We'll have to pull the name of an organization from the post data that comes in when the form is submitted. Next, we'll need to save the organization with the given name. Lastly, we'll have to supply the home page with a list of organizations. That's a tall order. Let's get started with some unit tests.

# organizations/tests/test_views.py

from ..models import Organization

class HomeViewTest(TestCase):
    ...

    def test_view_returns_organization_list(self):
        organization = Organization.objects.create(name='test')
        response = self.client.get('/')
        self.assertListEqual(response.context['organizations'], [organization])
Enter fullscreen mode Exit fullscreen mode

PyCharm already warns us that it cannot find the model, but let's run the unit test anyway. Of course, we get an import error. Let's create the model. Open the models.py file in the organizations folder and add the following code.

# organization/models.py

from django.db import models


class Organization(models.Model):
    name = models.CharField(max_length=250)
Enter fullscreen mode Exit fullscreen mode

PyCharm stops complaining, but what happens when we run the unit test? We get a programming error! We need to add the Organization model as a table to the database. Luckily, Django makes it really easy to do this via the terminal.

python manage.py makemigrations organizations
python manage.py migrate organizations
Enter fullscreen mode Exit fullscreen mode

Our unit tests are still failing, but the programming error is taken care of. Let's make our test pass with minimal code.

# organizations/views.py

from .models import Organization


def home_view(request):
    return render(request, 'organizations/home.html', {
        'organizations': list(Organization.objects.all())
    })
Enter fullscreen mode Exit fullscreen mode

That gets our unit tests passing. Organizations are being passed to the home page, but they are not being saved yet. Let's write another unit test to our create view.

# organizations/tests/test_views.py

class CreateViewTest(TestCase):
    ...

    def test_view_creates_organization_on_post(self):
        self.client.post('/create/', data={'name': 'test'})
        self.assertEqual(Organization.objects.count(), 1)
        organization = Organization.objects.last()
        self.assertEqual(organization.name, 'test')
Enter fullscreen mode Exit fullscreen mode

This test sends a post request to the create view with some data. The test then checks to make sure an organization is created and that it has the same name as the data sent. The unit test fails as expected. Let's write some code to make it pass.

# organizations/views.py

def create_view(request):
    if request.method == 'POST':
        name = request.POST.get('name', '')
        Organization.objects.create(name=name)
        return redirect(reverse('home'))

    return render(request, 'organizations/create.html')
Enter fullscreen mode Exit fullscreen mode

The unit tests pass. Let's try the functional tests. We're close, I can feel it. The functional test cannot find our organization, so we just need to adjust our home template.

<!-- organizations/templates/organizations/home.html -->

<table>
    <tbody>
        {% for organization in organizations %}
            <tr>
                <td>{{ organization.name }}</td>
            </tr>
        {% empty %}
            <tr>
                <td>No organizations</td>
            </tr>
        {% endfor %}
    </tbody>
</table>
Enter fullscreen mode Exit fullscreen mode

Our functional tests pass! Everything works! Let's commit our changes.

git add .
git commit -m "Added organization model. All tests passing."
Enter fullscreen mode Exit fullscreen mode

Refactoring Our Code

Let's take a look at our website. Wow, it's functional, but it's really ugly. Also, even though its functional, it's not functioning the most efficiently. We should be using forms to handle the data transfers and the template rendering. Let's make this site look pretty and let's add forms.

Adding Forms

We want to utilize Django forms in our application. On the front-end, we want the forms to render the controls that we expect (the input text field). We need to pass an instance of the form to the template. We also want to replace the clunky code that is extracting the post data and manually saving the model with something smoother. Let's handle the view first.

# organizations/tests/test_views.py

from ..forms import OrganizationForm

class CreateViewTest(models.Model):
    ...

    def test_view_returns_organization_form(self):
        response = self.client.get('/create/')
        self.assertIsInstance(response.context['form'], OrganizationForm)
Enter fullscreen mode Exit fullscreen mode

As expected, the unit tests fail with an import error. Let's create the form. Add a new Python file forms.py to the organizations folder and add the following code.

# organizations/forms.py

from django import forms


class OrganizationForm(forms.Form): pass
Enter fullscreen mode Exit fullscreen mode

The unit tests fail, but we don't get the import error. Add code to make the test pass.

# organizations/views.py

from .forms import OrganizationForm


def create_view(request):
    ...

    return render(request, 'organizations/create.html', {
        'form': OrganizationForm()
    })
Enter fullscreen mode Exit fullscreen mode

The unit test passes. Let's test the form to make sure that it renders the controls we want. Create a new Python file in the organizations/tests directory and call it test_forms.py. Add a test that checks to see if the text input control is present.

# organizations/tests/test_forms.py

from django.test import TestCase
from ..forms import OrganizationForm


class OrganizationFormTest(TestCase):
    def test_form_has_required_fields(self):
        form = OrganizationForm()
        self.assertIn('id="id_name"', form.as_p())

Enter fullscreen mode Exit fullscreen mode

Run the unit tests and see that one fails. We need to make the form use the Organization model. Adjust the OrganizationForm like the following.

# organizations/forms.py
from .models import Organization


class OrganizationForm(forms.ModelForm):
    class Meta:
        model = Organization
Enter fullscreen mode Exit fullscreen mode

The unit test passes, however, you might see a warning regarding ModelForms. Fix the form to get rid of that warning.

# organizations/forms.py

class OrganizationForm(forms.ModelForm):
    class Meta:
        model = Organization
        fields = ('name',)
Enter fullscreen mode Exit fullscreen mode

Return to our view unit tests. We need to replace the current logic, so that the form handles all of the data transfers. We need to add a couple of tests, but first let's install a new Python library.

pip install mock
Enter fullscreen mode Exit fullscreen mode

Let's add a couple new unit tests for the create view.

# organizations/tests/test_views.py

from mock import patch
from django.test import RequestFactory
from ..views import create_view


class CreateViewTest(TestCase):
    def setUp(self):
        self.factory = RequestFactory()

    ...

    @patch('organizations.views.OrganizationForm')
    def test_passes_post_data_to_form(self, mock_organization_form):
        request = self.factory.post('/create/', data={'name': 'test'})
        create_view(request)
        mock_organization_form.assert_any_call(data=request.POST)

    @patch('organizations.views.OrganizationForm')
    def test_saves_organization_for_valid_data(self, mock_organization_form):
        mock_form = mock_organization_form.return_value
        mock_form.is_valid.return_value = True
        mock_form.save.return_value = None
        request = self.factory.post('/create/', data={'name': 'test'})
        create_view(request)
        self.assertTrue(mock_form.save.called)
Enter fullscreen mode Exit fullscreen mode

Let's break down our changes. Notice that we are using a different strategy for these tests. Instead of using the Django client, we are using the RequestFactory to manufacture a Django HttpRequest and passing it to the view itself. In these new tests, our goal is not to test the functionality of the forms. We only want to test that the view is interacting with the form in the way it should. Our focus is on the view.

Our first test is confirming that the request data is being passed to the form. Before, we handled the data ourselves, so now we want to make sure that the form is actually being given the chance to handle it instead. We do this by temporarily overwriting the real for with a fake form. The patch function does this and passes the fake form object to unit test to use. The only thing we need to know is that the form in our view is being called with the post parameters.

Our second test requires a little more work. Again, we are mocking the form with a fake object, but this time, we have to add some fake functions to reflect the structure of the real form. When a form is used in a view, it has to validate the input and then save, in order to successfully create a new model object. In this unit test, we are mocking the is_valid() and save() methods, so that they operate the way we expect them to. We are then checking to make sure that the save function is called on the form.

The unit tests fail as expected. Let's add the code to make them pass.

# organizations/views.py

def create_view(request):
    form = OrganizationForm()

    if request.method == 'POST':
        form = OrganizationForm(data=request.POST)
        if form.is_valid():
            form.save()
        return redirect(reverse('home'))

    return render(request, 'organizations/create.html', {
        'form': form
    })
Enter fullscreen mode Exit fullscreen mode

We adjust the create_view() so that an empty form is created on every request, and the post data is passed to the form on a post request. We also add functionality to check that the form is valid and then to save it. The unit tests pass. Our last step is to adjust the create template, so that is uses the Django form to render the controls. We replace the hard-coded HTML with the form context.

<!-- organizations/templates/organizations/create.html -->

<form action="{% url 'create' %}" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button id="submit" type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

When we run the functional tests, we see that they fail. We need to add a placeholder attribute to the name field. Let's adjust our form.

# organizations/forms.py

class OrganizationForm(forms.ModelForm):
    class Meta:
        model = Organization
        fields = ('name',)
        widgets = {
            'name': forms.fields.TextInput(attrs={
                'placeholder': 'Organization name'
            })
        }
Enter fullscreen mode Exit fullscreen mode

Both the functional tests and the unit tests are passing. We've successfully implemented forms in our project! Let's commit our code.

git add .
git commit -m "Replaced template and view code with forms."
Enter fullscreen mode Exit fullscreen mode

Making the Application Look Pretty

Our code is now more efficient. Using forms allows us to avoid having to edit the template and the view every time we add more fields to the form. The code is solid, but the application still looks ugly. Let's spruce it up using Bootstrap components and styles. While we're at it, let's make a base template that the home and create templates can inherit from, so we avoid duplicate code. Create a new HTML file in the templates/organizations folder and name it base.html.

<!-- organizations/templates/organizations/base.html -->

<!DOCTYPE html>

<html>

<head lang="en">
    <meta charset="utf-8">
    <title>Test-Driven</title>
    <meta name="viewport"
          content="user-scalable=no, width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootswatch/3.3.2/yeti/bootstrap.min.css">
</head>

<body>
    <div class="container">
        {% block page-content %}{% endblock page-content %}
    </div>

    <script src="//code.jquery.com/jquery-2.1.3.min.js"></script>
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

We've added a <meta> element to control for the scaling that happens when a mobile device attempts to render a full page in the viewport. We've imported Bootstrap CSS and JS files and a jQuery dependency, and we've also opted to use a free Bootstrap theme to make our page look less generic. Within the body of the HTML, we've added a block template tag that can be overridden by templates that extend this base. Let's adjust the other templates.

<!-- organizations/templates/organizations/home.html -->

{% extends 'organizations/base.html' %}

{% block page-content %}
    <div class="row">
        <div class="col-md-offset-3 col-md-6">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h4 class="panel-title">Organizations</h4>
                </div>
                <div class="panel-body">
                    <a id="create-button" class="btn btn-default" href="{% url 'create' %}">
                        Create organization
                    </a>
                </div>
                <table class="table table-striped">
                    <thead>
                        <tr>
                            <td><strong>Name</strong></td>
                        </tr>
                    </thead>
                    <tbody>
                        {% for organization in organizations %}
                            <tr>
                                <td>{{ organization.name }}</td>
                            </tr>
                        {% empty %}
                            <tr>
                                <td>No organizations</td>
                            </tr>
                        {% endfor %}
                    </tbody>
                </table>
            </div>
        </div>
    </div>
{% endblock page-content %}
Enter fullscreen mode Exit fullscreen mode

We spruce up the home page so that everything fits in a nice panel in the center of the screen. The table has a more interesting look with the striped style.

<!-- organizations/templates/organizations/create.html -->

{% extends 'organizations/base.html' %}

{% block page-content %}
    <div class="row">
        <div class="col-md-offset-3 col-md-6">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h4 class="panel-title">Create an organization</h4>
                </div>
                <div class="panel-body">
                    <form action="{% url 'create' %}" method="post">
                        {% csrf_token %}
                        {% for field in form %}
                            <div class="form-group">
                                {{ field.label_tag }}
                                {{ field }}
                            </div>
                        {% endfor %}
                        <button id="submit" class="btn btn-default" type="submit">Submit</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
{% endblock page-content %}
Enter fullscreen mode Exit fullscreen mode

We've given a similar treatment to the create template, putting the form into a panel. In order to get our form to render with Bootstrap styling, we have a couple options. Once choice is to use a third-party library like Crispy Forms. I've chosen to implement it manually, by adding a mixin to the form class.

# organizations/forms.py

class BootstrapMixin(object):
    def __init__(self, *args, **kwargs):
        super(BootstrapMixin, self).__init__(*args, **kwargs)

        for key in self.fields:
            self.fields[key].widget.attrs.update({
                'class': 'form-control'
            })


class OrganizationForm(BootstrapMixin, forms.ModelForm): ...
Enter fullscreen mode Exit fullscreen mode

Let's run all of our functional and unit tests one last time. They pass as expected. Let's visit our page and take a look. It's a lot prettier. Commit the visual changes to Git.

git add .
git commit -m "Made the templates prettier with Bootstrap."
Enter fullscreen mode Exit fullscreen mode

Visit your website and try out the functionality. If you want to deploy your site or share it with others, make sure to add a remote Git repository and push your code. Also, freeze your requirements and include them in a document to make duplication of your virtual environment easier.

pip freeze > requirements.txt
git add .
git commit -m "Added requirements document."
git remote add origin git@your_git_repository
git push -u origin master
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
osahenru profile image
Osahenru

Good write up, but I don't see the attached mock-up wire frame