Test-Driven Development With Django - Sample Chapter
Test-Driven Development With Django - Sample Chapter
Test-Driven Development
with Django
ee
Sa
m
pl
C o m m u n i t y
E x p e r i e n c e
D i s t i l l e d
Test-Driven Development
with Django
$ 29.99 US
19.99 UK
P U B L I S H I N G
Kevin Harvey
Kevin Harvey
in 2007. His professional interests include software quality, open source, and teaching.
He continues to be amazed at the Python community's ability to turn a history major
into a software engineer, a feat for which he will forever be indebted. When not writing
unit tests, Kevin enjoys playing the bass (both electric and stand up), and cooking with
entirely too much butter. He lives in Nashville, Tennessee, with his wife and their
two sons.
Preface
Writing software is hard. Even the smallest projects have many moving parts.
We developers are not only expected to get those parts moving, but keep them
moving as the application changes over time. Test-Driven Development (TDD) is
a methodology that allows us to quantify the successful function of each of these
parts before we attempt to code them. Using TDD, we can focus on a single part
of the application at a time, leaving a trail of tests that guard against regression as
we continue to update the application.
Django is a popular web framework written in Python. Its batteries-included: the
framework itself includes URL routing, object-relational mapping, templates, and
many other necessities for building a modern web application. This book will take
you through the process of developing a Django app by writing failing tests first,
then writing application code to make those tests pass.
Preface
Chapter 4, Building Out and Refactoring, adds new features to the application. We'll use
our test suite to maintain existing functionality while refactoring to keep our code
tidy and maintainable.
Chapter 5, User Stories As Code, focuses on LiveServerTestCase and the Python
Selenium bindings we use to drive the browser during a test run. We'll learn how
to select and click on elements, submit forms, switch between open windows, and
perform other user actions in our UI.
Chapter 6, No App Is an Island, applies the TDD methodology to third-party API
integration. We'll learn when, why, and how to mock out HTTP requests inside a
single unit test so that our tests aren't relying on an outside resource (even if our
app is).
Chapter 7, Share and Share Alike, introduces Django REST Frameworka tool for
building a REST API with Django. We'll cover the importance of documentation
when writing an API, and use the framework's tools to send requests to the API
during tests.
Chapter 8, Promises Kept, takes a look back at what we've learned, and whether we've
realized all the benefits from the first chapter. We'll get suggestions for next steps in
TDD, and talk about some of the common pitfalls you may encounter.
[1]
A simple example
Here's a quick example using Python's built-in assert, a statement that evaluates
a condition. It will throw an AssertionError if the condition is false, and returns
nothing otherwise.
Let's say we wanted a Python function that could multiply two numbers together
and return the result. Let's call it multiplicator.
The first step in TDD, before writing any code, is to find a way to test the application
you want to write. If you're having trouble coming up with a test scenario, imagine
that you've already written the application (in this case, that single function) and
want to try it out in the command line. You'd probably do something like this:
$ python
>>> from multiplicator import multiplicator
>>> multiplicator(2, 3)
6
You, the human, would look at the output of the function call (6) and confirm that
the operation was performed successfully. How can we teach our application to do
this confirmation itself? Enter assert. Create a file called multiplicator.py and
enter the following code:
# multiplicator.py
assert multiplicator(2, 3) == 6
We can translate this statement into English as "run multiplicator with arguments
2 and 3 and throw an error if the returned value does not equal 6."
We'll get into the more interesting tools available in the unittest
library in Chapter 2, Your First Test-Driven Application. For now, this
is all we need to see how TDD works.
We now have a runnable test for our function, without so much as an attempt to
write the function itself. Let's run it and see what happens:
$ python multiplicator.py
Traceback (most recent call last):
File "multiplicator.py", line 1, in <module>
assert multiplicator(2, 3) == 6
NameError: name 'multiplicator' is not defined
[2]
Chapter 1
Looks like Python can't find anything called multiplicator. We can fix that with
the following code:
# multiplicator.py
def multiplicator():
pass
assert multiplicator(2,3) == 6
Okay, our function needs to accept some arguments. Let's update it:
# multiplicator.py
def multiplicator(x, y):
pass
assert multiplicator(2, 3) == 6
This AssertionError is the one we asked our test to throw (via assert) if the result
of our function did not equal the expected value (6). Now that we're here, we can
write some logic:
# multiplicator.py
def multiplicator(x, y):
i = 0
result = 0
while i < x:
result += y
i += 1
[3]
Whoa there, Tex! That's one way to do it I suppose. Should we run the tests?
$ python multiplicator.py
Huh? No output? No error? You mean that monstrosity actually made the test pass?
Yes it did! We wrote application code to make the test pass without any pressure to
optimize it, or without picking the best Python function to make it work. Now that
we have the test passing, we can optimize to our heart's content; we know we're safe
as long as the test continues to pass.
Let's update multiplicator to use some of Python's own integer arithmetic:
# multiplicator.py
def multiplicator(x, y):
return x*y
assert multiplicator(2, 3) == 6
That's better. We built a working, optimized function and a test suite to check for
regressions using basic TDD methodology. This process is often referred to as "red/
green/refactor".
Red/green/refactor
The smallest cycle of TDD typically involves three steps:
1. Writing a test that fails (red).
2. Doing whatever is necessary to get that test to pass (green).
3. Optimizing to fix any subpar code you may have introduced to get the test to
pass (refactor).
In the preceding example, we wrote a test for the desired functionality and watched
it fail (red). Then we wrote some less-than-optimal code to get it to pass (green).
Finally, we refactored the code to simplify the logic while keeping the test passing
(refactor). This virtuous circle is how TDD helps you write code that is both
functional and beautiful.
[4]
Chapter 1
Version control
Version control is the ultimate undo button. It allows you to check code changes into
a repository at regular intervals and rollback to any of those changes later. We'll be
using Git throughout the course of this book. To get a good primer on Git, check out
http://git-scm.com/doc.
Documentation
If we're using TDD to keep promises, documentation is where we first make these
promises. Simply put, documentation describes how your application works. At a
minimum, your software project needs the development documentation for the next
person to maintain it (even if it's you, you'll forget what you wrote). You'll probably
need some less technical documentation for the end user as well.
Testing
Testing and documentation have a crucial relationshipyour tests prove that your
documentation is telling the truth. For instance, the documentation for a REST API
may instruct a developer to send a POST request to a given URL, with a certain JSON
payload in order to get back a certain response. You can ensure this is what happens
by exercising this specific behavior in your tests.
[5]
Continuous Integration
All of these glorious tests will be pretty useless if no one is running them. Luckily,
actually running the tests (and alerting us of any failures) is another thing we can train
a machine to do. A Continuous Integration (CI) server, for our purposes, can pull
our project from version control, build it, run our tests, and alert us if any errors or
failures occur. It can also be the first place where our tests are run in a production-like
environment (for instance, in the same operating system and database configuration),
allowing us to keep our local environments configured for speed and ease.
It will keep you on track: Writing the tests first is like keeping an executable
checklist of all the development tasks you have to complete. Good functional
tests are the key link between user stories (which is what everyone really
cares about) and your code. A well-designed functional test will ensure
that the end user will be able to do everything they need to do with your
application.
You will build exactly (and only) what is required: As we'll see in Chapter 2,
Your First Test-Driven Application, a good first step in Test-Driven Development
is the translation of a user story into a distinct, self-contained functional test.
Codifying the project's requirements as a test and only writing enough code
to make that test pass will ensure that you've fulfilled all the user stories and
guard against any scope creep. The project itself will help you determine when
development is complete, or if any changes introduced later would interfere
with any end-user functionality.
[6]
Chapter 1
Post-development tests just don't have the same weight: If you try to write
a test for some code that already does what you want, you'd have already
closed your mind to the other possibilities of that code. You'll wind up with
a narrow test that only covers that aspect of the code that you were thinking
about while you were writing it. Writing the test when you're free of any
preconceptions will yield a test that's more comprehensive, which will in
turn produce stronger, less buggy code.
You will achieve flow: TDD is all about incremental improvement. Each new
test that passes (or incremental step to get to the next error in a test) is a little
win. Plus, you won't have to spend hours debugging if you mess something
up and a test fails. You'll be able to go right to the problem because the test
that you wrote before you built that part of the application will be the one
that failed.
Have you ever worked on a project where considerable effort went into
maintaining a "development" database? Maybe it was set up so that you
could check the effect of a custom save method from time to time? Or maybe
you needed to dive into ./manage.py shell, import a bunch of your code,
instantiate a few models, and then run your method to see if it worked?
There's no monkey business like this when you write the tests first. The
application state that you need is codified in your test suite. All that set up
will happen in one command and on every run (not just when you're futzing
with that method).
No one will ever know how buggy your code started out: If you've worked
on software projects of any complexity, you've probably developed a
healthy fear of change. Change breaks stuff, particularly change to a part
of an application that finds itself imported all over your project. However,
if you've developed the entire application writing tests first, you've left a
trail of test coverage that will alert you well before that bug you just wrote
gets in to source control, let alone deployed. TDD allows you to refactor and
optimize without fear of regression.
[7]
Bugs will stay fixed: If I write a failing test that demonstrates a bug report
that I receive, then update my application to make the test pass, I'll never
have to worry about that bug coming back ever again because my test will
catch it. Less time worrying about my production application means more
fearless feature development.
You'll write testable code: Code that is easily tested is better code. It seems
both silly and obvious but it's worth mentioning. If you can prove beyond
a shadow of a doubt that your code has the desired effect or return value,
you'll be better able to maintain it. Writing the test before you write the
code will force you to write code that can be easily tested.
[8]
Chapter 1
You'll be able to take big risks: We've all been therelate in the development
process or even after shipment, we see a tweak that we'd like to make in a
linchpin model or method. The tweak would be a tremendous boon to system
performance, but the change would have an unknown effect on nearly every
other part of the application. If we've followed TDD, we'll have a complete test
suite that will allow us to know the ramifications of that change immediately.
We'll be able to make the change, run the tests, and see early on what it would
take to keep the rest of the system in place.
You'll look like a pro: When you release your code out into the world, either
as a user-facing application or an installable package for other developers,
you're making a promise to the people that use it. You're promising that the
documentation was in fact accurate and that the dang thing does what it's
supposed to do. A comprehensive test suite helps keep that promise and
there's no better way to build one than by following the TDD mantra.
Particularly in the open source world, the presence of a test suite lets the
community know that you're serious. It's the first thing you should look for
when evaluating a new PyPI package to install. A test suite says that you
can trust this software.
Now let's talk about the long term. Towards the end of the project, or even after
launch, a big change will come down from the product owner (this is Agile, right?)
or you'll find something fundamental that you want to modify. The comprehensive
test suite you've built through TDD will pay you back in spades when something
goes wrong, or if you need to refactor. The flexibility provided by your test suite will
likely save you more time than you spent creating it. You'll thank TDD in the end.
Summary
In this chapter, we introduced the practice of TDD and the benefits of using it.
In the next chapter, we will start a Django project from scratch using rigorous
TDD methodology, learning some of the testing tools available in Django and
Python along the way.
[ 10 ]
www.PacktPub.com
Stay Connected: