-
-
Save tonykevin/804f82f862a702065646 to your computer and use it in GitHub Desktop.
Full stack BDD testing with Behave+Mechanize+Django
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
*.pyc | |
bin/ | |
include/ | |
lib/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Feature: Demonstrate how to use the mechanize browser to do useful things. | |
Scenario: Logging in to our new Django site | |
Given a user | |
When I log in | |
Then I see my account summary | |
And I see a warm and welcoming message | |
Scenario: Loggout out of our new Django site | |
Given a user | |
When I log in | |
And I log out | |
Then I see a cold and heartless message |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
"""steps/browser_steps.py -- step implementation for our browser feature demonstration. | |
""" | |
from behave import given, when, then | |
@given('a user') | |
def step(context): | |
from django.contrib.auth.models import User | |
u = User(username='foo', email='[email protected]') | |
u.set_password('bar') | |
u.save() | |
@when('I log in') | |
def step(context): | |
br = context.browser | |
br.open(context.browser_url('/account/login/')) | |
br.select_form(nr=0) | |
br.form['username'] = 'foo' | |
br.form['password'] = 'bar' | |
br.submit() | |
@then('I see my account summary') | |
def step(context): | |
br = context.browser | |
response = br.response() | |
assert response.code == 200 | |
assert br.geturl().endswith('/account/'), br.geturl() | |
@then('I see a warm and welcoming message') | |
def step(context): | |
# Remember, context.parse_soup() parses the current response in | |
# the mechanize browser. | |
soup = context.parse_soup() | |
msg = str(soup.findAll('h2', attrs={'class': 'welcome'})[0]) | |
assert "Welcome, foo!" in msg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
"""environment -- environmental setup for Django+Behave+Mechanize | |
This should go in features/environment.py | |
(http://packages.python.org/behave/tutorial.html#environmental-controls) | |
Requirements: | |
http://pypi.python.org/pypi/behave/ | |
http://pypi.python.org/pypi/mechanize/ | |
http://pypi.python.org/pypi/wsgi_intercept | |
http://pypi.python.org/pypi/BeautifulSoup/ | |
Acknowledgements: | |
For the basic solution: https://github.com/nathforge/django-mechanize/ | |
""" | |
import os | |
# This is necessary for all installed apps to be recognized, for some reason. | |
os.environ['DJANGO_SETTINGS_MODULE'] = 'myproject.settings' | |
def before_all(context): | |
# Even though DJANGO_SETTINGS_MODULE is set, this may still be | |
# necessary. Or it may be simple CYA insurance. | |
from django.core.management import setup_environ | |
from myproject import settings | |
setup_environ(settings) | |
from django.test import utils | |
utils.setup_test_environment() | |
## If you use South for migrations, uncomment this to monkeypatch | |
## syncdb to get migrations to run. | |
# from south.management.commands import patch_for_test_db_setup | |
# patch_for_test_db_setup() | |
### Set up the WSGI intercept "port". | |
import wsgi_intercept | |
from django.core.handlers.wsgi import WSGIHandler | |
host = context.host = 'localhost' | |
port = context.port = getattr(settings, 'TESTING_MECHANIZE_INTERCEPT_PORT', 17681) | |
# NOTE: Nothing is actually listening on this port. wsgi_intercept | |
# monkeypatches the networking internals to use a fake socket when | |
# connecting to this port. | |
wsgi_intercept.add_wsgi_intercept(host, port, WSGIHandler) | |
import urlparse | |
def browser_url(url): | |
"""Create a URL for the virtual WSGI server. | |
e.g context.browser_url('/'), context.browser_url(reverse('my_view')) | |
""" | |
return urlparse.urljoin('http://%s:%d/' % (host, port), url) | |
context.browser_url = browser_url | |
### BeautifulSoup is handy to have nearby. (Substitute lxml or html5lib as you see fit) | |
from BeautifulSoup import BeautifulSoup | |
def parse_soup(): | |
"""Use BeautifulSoup to parse the current response and return the DOM tree. | |
""" | |
r = context.browser.response() | |
html = r.read() | |
r.seek(0) | |
return BeautifulSoup(html) | |
context.parse_soup = parse_soup | |
def before_scenario(context, scenario): | |
# Set up the scenario test environment | |
# We must set up and tear down the entire database between | |
# scenarios. We can't just use db transactions, as Django's | |
# TestClient does, if we're doing full-stack tests with Mechanize, | |
# because Django closes the db connection after finishing the HTTP | |
# response. | |
from django.db import connection | |
connection.creation.create_test_db(verbosity=1, autoclobber=True) | |
### Set up the Mechanize browser. | |
from wsgi_intercept import mechanize_intercept | |
# MAGIC: All requests made by this monkeypatched browser to the magic | |
# host and port will be intercepted by wsgi_intercept via a | |
# fake socket and routed to Django's WSGI interface. | |
browser = context.browser = mechanize_intercept.Browser() | |
browser.set_handle_robots(False) | |
def after_scenario(context, scenario): | |
# Tear down the scenario test environment. | |
from django.db import connection | |
connection.creation.destroy_test_db(verbosity=1, autoclobber=True) | |
# Bob's your uncle. | |
def after_all(context): | |
from django.test import utils | |
utils.teardown_test_environment() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> | |
<html> <head> | |
<title></title> | |
</head> | |
<body> | |
<h2 class="welcome">Welcome, foo!</h2> | |
</body> </html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
I, David Eyk, hereby dedicate this work to the public domain by waiving all of my rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. | |
You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. See http://creativecommons.org/publicdomain/zero/1.0/ for a full summary and legal text. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> | |
<html> <head> | |
<title></title> | |
</head> | |
<body> | |
<form action="." method="post"> | |
{% csrf_token %} | |
<input name="username" /> | |
<input name="password" /> | |
<input type="submit" value="Submit"> | |
</form> | |
</body> </html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Django<1.5 | |
psycopg2 | |
behave | |
mechanize | |
wsgi_intercept | |
BeautifulSoup |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Django settings for myproject project. | |
import os | |
DEBUG = True | |
TEMPLATE_DEBUG = DEBUG | |
ADMINS = ( | |
# ('Your Name', '[email protected]'), | |
) | |
MANAGERS = ADMINS | |
DATABASES = { | |
'default': { | |
'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. | |
'NAME': 'myproject', # Or path to database file if using sqlite3. | |
'USER': 'myuser', # Not used with sqlite3. | |
'PASSWORD': 'mypassword', # Not used with sqlite3. | |
'HOST': 'localhost', # Set to empty string for localhost. Not used with sqlite3. | |
'PORT': '5432', # Set to empty string for default. Not used with sqlite3. | |
} | |
} | |
# Local time zone for this installation. Choices can be found here: | |
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name | |
# although not all choices may be available on all operating systems. | |
# On Unix systems, a value of None will cause Django to use the same | |
# timezone as the operating system. | |
# If running in a Windows environment this must be set to the same as your | |
# system time zone. | |
TIME_ZONE = 'America/Chicago' | |
# Language code for this installation. All choices can be found here: | |
# http://www.i18nguy.com/unicode/language-identifiers.html | |
LANGUAGE_CODE = 'en-us' | |
SITE_ID = 1 | |
# If you set this to False, Django will make some optimizations so as not | |
# to load the internationalization machinery. | |
USE_I18N = True | |
# If you set this to False, Django will not format dates, numbers and | |
# calendars according to the current locale. | |
USE_L10N = True | |
# If you set this to False, Django will not use timezone-aware datetimes. | |
USE_TZ = True | |
# Absolute filesystem path to the directory that will hold user-uploaded files. | |
# Example: "/home/media/media.lawrence.com/media/" | |
MEDIA_ROOT = '' | |
# URL that handles the media served from MEDIA_ROOT. Make sure to use a | |
# trailing slash. | |
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" | |
MEDIA_URL = '' | |
# Absolute path to the directory static files should be collected to. | |
# Don't put anything in this directory yourself; store your static files | |
# in apps' "static/" subdirectories and in STATICFILES_DIRS. | |
# Example: "/home/media/media.lawrence.com/static/" | |
STATIC_ROOT = '' | |
# URL prefix for static files. | |
# Example: "http://media.lawrence.com/static/" | |
STATIC_URL = '/static/' | |
# Additional locations of static files | |
STATICFILES_DIRS = ( | |
# Put strings here, like "/home/html/static" or "C:/www/django/static". | |
# Always use forward slashes, even on Windows. | |
# Don't forget to use absolute paths, not relative paths. | |
) | |
# List of finder classes that know how to find static files in | |
# various locations. | |
STATICFILES_FINDERS = ( | |
'django.contrib.staticfiles.finders.FileSystemFinder', | |
'django.contrib.staticfiles.finders.AppDirectoriesFinder', | |
# 'django.contrib.staticfiles.finders.DefaultStorageFinder', | |
) | |
LOGIN_REDIRECT_URL = '/account/' | |
# Make this unique, and don't share it with anybody. | |
SECRET_KEY = 'gz6b+p%k*_lty%v#8g%-70ndzd@h&v_tp&s)78s%$m)qlc=06b' | |
# List of callables that know how to import templates from various sources. | |
TEMPLATE_LOADERS = ( | |
'django.template.loaders.filesystem.Loader', | |
'django.template.loaders.app_directories.Loader', | |
# 'django.template.loaders.eggs.Loader', | |
) | |
MIDDLEWARE_CLASSES = ( | |
'django.middleware.common.CommonMiddleware', | |
'django.contrib.sessions.middleware.SessionMiddleware', | |
'django.middleware.csrf.CsrfViewMiddleware', | |
'django.contrib.auth.middleware.AuthenticationMiddleware', | |
'django.contrib.messages.middleware.MessageMiddleware', | |
# Uncomment the next line for simple clickjacking protection: | |
# 'django.middleware.clickjacking.XFrameOptionsMiddleware', | |
) | |
ROOT_URLCONF = 'myproject.urls' | |
# Python dotted path to the WSGI application used by Django's runserver. | |
WSGI_APPLICATION = 'myproject.wsgi.application' | |
TEMPLATE_DIRS = ( | |
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". | |
# Always use forward slashes, even on Windows. | |
# Don't forget to use absolute paths, not relative paths. | |
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates'), | |
) | |
INSTALLED_APPS = ( | |
'django.contrib.auth', | |
'django.contrib.contenttypes', | |
'django.contrib.sessions', | |
'django.contrib.sites', | |
'django.contrib.messages', | |
'django.contrib.staticfiles', | |
# Uncomment the next line to enable the admin: | |
# 'django.contrib.admin', | |
# Uncomment the next line to enable admin documentation: | |
# 'django.contrib.admindocs', | |
) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.conf.urls.defaults import patterns, include, url | |
from django.views.generic.simple import direct_to_template | |
urlpatterns = patterns('', | |
url(r'account/', include('django.contrib.auth.urls')), | |
url('^account/$', direct_to_template, { | |
'template': 'home.html' | |
}, name='home') | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment