This is the third of a series of posts loosely associated with integration testing, mostly focused on Python.
Context managers are a way of allocating and releasing some sort of resource exactly where you need it. The simplest example is file access:
with file(“/tmp/foo", “w”) as foo: print >> foo, “Hello!”
This is essentially equivalent to:
foo = file(“/tmp/foo", “w”) try: print >> foo, “Hello!” finally: foo.close()
Locks are another example. Given:
import threading lock = threading.Lock()
with lock: my_list.append(item)
replaces the more verbose:
lock.acquire() try: my_list.append(item) finally: lock.release()
In each case a bit of boilerplate is eliminated, and the “context”
of the file or the lock is acquired, used, and released cleanly.Context managers are a common idiom in Lisp, where they are usually
defined using macros (examples are
with-output-to-string in Common
Python, not having macros, must include context managers as part of
the language. Since 2.5, it does so, providing an easy mechanism for
rolling your own. Though the default, “low level” way to make a
context manager is to make a class which follows the context management protocol, by
the simplest way is using the
contextmanager decorator from the
contextlib library, and invoking
yield in your context manager
function in between the setup and teardown steps.
Here is a context manager which you could use to time our threads-vs-processes testing, discussed previously:
import contextlib import time @contextlib.contextmanager def time_print(task_name): t = time.time() try: yield finally: print task_name, “took", time.time() - t, “seconds.” with time_print(“processes”): [doproc() for _ in range(500)] # processes took 15.236166954 seconds. with time_print(“threads”): [dothread() for _ in range(500)] # threads took 0.11357998848 seconds.
Context managers can be composed very nicely. While you can certainly do the following,
with a(x, y) as A: with b(z) as B: # Do stuff
in Python 2.7 or above, the following also works:
with a(x, y) as A, b(z) as B: # Do stuff
with Python 2.6, using
contextlib.nested does almost
the same thing:
with contextlib.nested(a(x, y), b(z)) as (A, B): # Do the same stuff
the difference being that with the 2.7+ syntax, you can use the
value yielded from the first context manager as the argument to the
with a(x, y) as A, b(A) as C:...).
If multiple contexts occur together repeatedly, you can also roll them together into a new context manager:
import contextlib @contextlib.contextmanager def c(x, y, z): with a(x, y) as A, b(z) as B: yield (A, B) with c(x, y, z) as C: # C == (A, B) # Do that same old stuff
What does all this have to do with testing? I have found that for complex integration tests where there is a lot of setup and teardown, context managers provide a helpful pattern for making compact, simple code, by putting the “context” (state) needed for any given test close to where it is actually needed (and not everywhere else). Careful isolation of state is an important aspect of functional programming.
More on the use of context managers in actual integration tests in the next post.