Files
www.gittip.com/tests/test_suite_intro.py
2012-10-17 10:41:07 -04:00

153 lines
5.5 KiB
Python

"""This is a demonstration script for Gittip's test suite.
"""
# Layout
# ======
# The Gittip test suite lives at tests/test_*.py. The tests/ directory is not a
# Python package (there is no __init__.py). Think of it as holding test scripts
# to be run via nosetest (maybe py.test would work too?). Helpers are defined
# in the gittip.testing module.
from gittip import testing
# Basic Pattern
# =============
# First, import something from the gittip library. Here I'm defining a function
# inline for demonstration purposes.
def greet():
return "Greetings, program!"
# Then, write a test case. Here's what a canonical test case in the Gittip test
# suite looks like:
def test_greet_greets_programs():
expected = "Greetings, program!"
actual = greet()
assert actual == expected, actual
# The name of the test case should be a sentence, with a subject and a
# predicate. The subject should be the thing under test, and the predicate
# should state the expected behavior we're testing for.
def test_greet_still_greets_programs():
# More complex tests will start with some setup. Here in this test we don't
# have any. The last three lines of each test case look like the following.
# Ask questions first, shoot later: our expectation always preceeds the
# performance of the test.
expected = "Greetings, program!"
# Perform the test, storing the result in actual.
actual = greet()
# Compare reality with our expectation, and, if they don't match, inform
# the viewer of reality.
assert actual == expected, actual
# Context Managers
# ================
# Gittip's test suite uses context managers to manage testing state instead of
# test classes with setup/teardown methods. The reason is to make the test
# suite flatter and easier to follow, and to keep a tight coupling between test
# fixture and test cases. We want to avoid bloated super-fixtures.
def test_inserting_inserts():
# Gittip's fundamental context manager for testing is gittip.testing.load.
# It's called that because its primary function is to load data into the
# database. When the context manager exits, the database is wiped.
with testing.load() as context:
# The context object gives you access to the database. The db attribute
# here is the usual PostgresManager that is used throughout Gittip.
context.db.execute("INSERT INTO participants VALUES ('foo')")
# There's a dump method on context that gives you all the data in the
# database, as a mapping of table names to mappings of id to row dict.
actual = context.dump()
# The context.diff method gives you a diff of the state of the database
# since you entered the context. With compact=True it returns a mapping
# of the names of tables that have changed, to a list of ints showing
# the number of rows that have been inserted, updated, and deleted,
# respectively.
actual = context.diff(compact=True)
# If the expectation can be stated succinctly, it's acceptable to
# inline it in the assertion, rather than defining it separately.
assert actual == {"participants": [1,0,0]}, actual
def test_something_changes_something():
# The testing.load callable takes a data definition as positional
# arguments. {str,unicode} is interpreted as a table name, and {dict,list,
# tuple} is interpreted as a row of data to be inserted into the most
# recently named table. Generally you'll end up defining "data" and then
# calling testing.load(*data), as it won't fit on one line.
with testing.load("participants", ("foo",)) as context:
context.db.execute("UPDATE participants SET statement='BLAM!!!' "
"WHERE id='foo'")
# Calling context.diff without compact=True gives you a mapping of the
# names of tables that have changed to a mapping with keys 'inserts',
# 'updates', and 'deletes'. The values for inserts and deletes are
# lists of row dicts containing the new and old data, respectively. The
# value for updates is a list of dicts containing only the data that
# has changed (and the primary key).
expected = {"id": "foo", "statement": "BLAM!!!"}
actual = context.diff()['participants']['updates'][0]
assert actual == expected, actual
# Wrappers
# --------
# As a rule of thumb, test cases should have one assertion each. Write wrappers
# for test cases that want slightly varying but similar state. Start by writing
# them in the same file as the test cases, and if they turn out to be useful in
# multiple test scripts, we'll move them into gittip.testing.
def let_them_eat_cake(): # For demonstration; this would be imported.
"""Simulate the gittip application doing something.
"""
import gittip
rec = gittip.db.fetchone('SELECT id FROM participants')
return "{id} eats cake.".format(**rec)
def participant(participant_id): # This is a context wrapper.
"""Wrap testing.load to install a participant.
"""
context = testing.load("participants", (participant_id,))
return context
def test_foo_eats_cake():
with participant("foo"):
actual = let_them_eat_cake()
assert actual == "foo eats cake.", actual
def test_bar_eats_cake():
with participant("bar"):
actual = let_them_eat_cake()
assert actual == "bar eats cake.", actual
# NB: There is one line between related test cases instead of two, as a way to
# group them together.