Test Driven Development with Python

Testing a Simple Home Page with Unit Tests

Unit Tests, and How They Differ from Functional Tests

The basic distinction, though, is that functional tests test the application from the outside, from the pont of view of the user.
Unit tests test the application from the inside, from the point of view of the programmer.

workflow

  1. We start by writing a functional test, describing the new functionality from the user’s point of view.
  2. Once we have a functional test that fails, we start to think about how to write code that can get it to pass(or at least to get past its current failure). We now use one or more unit tests to define how we want our code to behave - the idea is that each line of production code we write should be tested by (at least) one of our unit tests.
  3. Once we have a failing unit test, we write the smallest amount of application code we can, just enough to get the unit test to pass. We may iterate between steps 2 and 3 a few times, until we think the functional test wil get a little further.
  4. Now we can rerun our functional tests and see if they pass, or get a littel further. That may prompt us to wirte some new unit tests, and some noe code, and so on.

You can see that, all the way through, the functional test are driving what Development we do from a high level, while the unit tests drive what we do at a low level.

Note


functional tests should help you build an application with the right functionality, and guarantee you never accidentally break it. Unit tests should help you to write code that’s clean and bug free.


Django’s workflow

  1. An HTTP request comes in for a particular URL.
  2. Django uses some rules to decide which view function should deal with the request (this is refereed to as resolving the URL).
  3. The view function processes the request and returns an HTTP response.

So we want to test two things:

  • Can we resolve the URL for the root of the site ("/") to a particular view function we’ve made?
  • Can we make this view function return some HTML which will get the functional test to pass?

READING TRACEBACKS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
======================================================================
ERROR: test_root_url_resolves_to_home_page_view (lists.tests.HomePageTest) 2
---------------------------------------------------------------------
Traceback (most recent call last):
File "/.../superlists/lists/tests.py", line 8, in
test_root_url_resolves_to_home_page_view
found = resolve('/') 3
File ".../django/urls/base.py", line 27, in resolve
return get_resolver(urlconf).resolve(path)
File ".../django/urls/resolvers.py", line 392, in resolve
raise Resolver404({'tried': tried, 'path': new_path})
django.urls.exceptions.Resolver404: {'tried': [[<RegexURLResolver 1
<RegexURLPattern list> (admin:admin) ^admin/>]], 'path': ''} 1
---------------------------------------------------------------------
[...]
  1. The first place you look is usually the error itself. Sometimes that’s all you need to see, and it will let you identify the problem immediately. But sometimes, like in this case, it’s not quite self-evident.
  2. The next thing to double-check is: which test is failing? Is it definitely the one we expected - that is, the one we just wrote? In this case, the answer is yes.
  3. Then we look for the place in our test code that kicked off the failure. We work our way down form the top of the traceback, looking for the filename of the tests file, to check which test function, and what line of code, the failure is coming from. In thins case it’s the line where we call the resolve function for the “/” URL.

Pulling it all together, we interpret the traceback as telling us that, when trying to resolve “/”, Django raised a 404 error - in other words, Django can’t find a URL mapping for “/”.


The Unit-Test/Code Cycle

We can start to settle into the TDD unit-test/code cycle now:

  1. Run the unit tests in the terminal, run the unit tests and see how they fail.
  2. Mke a minimal code change in the editor, make a minimal code change to address the current test failure.
  3. Repeat!!!

What Are We Doing wiht All These Tests?(And, Refactoring)

Using Selenium to Test User Interactions

NOTE

One of the greate things about TDD is that you never have to worry about forgetting what to do next - just rerun your tests and they will tell you what you need to work on.


The “Don’t Test Constants” Rule, and Templates to the Rescue

In general, one of the rules of unit testing is Don’t test constants, and testing HTML as text is a lot like testing a constant.

In other words, if you have some code that says:

1
wibble = 3

There’s not much point in a test that says:

1
2
from myprogram import wibble
assert wibble == 3

Unit tests are really about testing logic, flow control, and configuration. Making assertions about exactly what sequence of characters we have in our HTML strings isn’t doing that.

What’s more, mangling raw strings in Python really isn’t a great way of dealing with HTML. There’s a much better solution, which is to use templates. Quite apart from anythin else, if we can keep HTML to one side in a file whose name ends in .html, we’ll get better syntax highlighting! There are lots of Python templating frameworks out there and Django has its own which works very well.

Refactoring to Use a Template

The first rule is that you can’t refactor without tests.


On Refactoring

TIP


When refactoring, work on either the code or the tests, but not both at once.


Recap: The TDD Process

  • Functional Tests
  • Unit Tests
  • The unit-test/code Cycle
  • Refactoring

We write a test. We run the test and see it fail. We write some minimal code to get it a little further. We rerun the test and repeat until it passes. Then, optionally, we might refactor our code, using tests to make sure we don’t break anything.

TDD process

But how does this apply when we have functional tests and unit tests? Well, you can think of the functional test as being a high-level view of the cycle, where “writing the code” to get the functional tests to pass actually involves using another, smaller TDD cycle which uses unit tests.

TDD process with functional and unit tests

What about refactoring, in the context of functional tests? Well, that means we use the functional test to check that we’ve preserved the behaviour of our application, but we can change or add and remove unit tests, and use a unit test cycle to acturally change the implementation.

The functional tests are the ultimate judge of whether your application works or not. The unit tests are a tool to help you along the way.

This way of looking at things is sometimes called “Double-Loop TDD”. one of my eminent tech reviewers, Emily Bache, wrote a blog post on the topic, which I recommend for a different perspective.

Saving User Input: Testing the Database

When a functional test fails with an unexpected failure, there ara several things we can do to debug it:

  • Add print statements, to show, for example, what the current page text is.
  • Improve the error message to show more info about the current state.
  • Manually visit the site yourself.
  • Use time.sleep to pause the test during execution.

Links

Obey the Testing Goat!

repo

Share