Skip to content

Instantly share code, notes, and snippets.

@amcgregor
Created November 4, 2011 04:31
Show Gist options
  • Save amcgregor/1338661 to your computer and use it in GitHub Desktop.
Save amcgregor/1338661 to your computer and use it in GitHub Desktop.
BDD Python DSL

What I propose below is a Behaviour (or Story) Driven Development domain-specific language for writing your tests in Python. This uses some tricks of how imports are done (via encoding) to dynamically translate the tests from the DSL into pure Python. Additional tricks are used to preserve the line numbers of errors, offer abbreviated asserts, and automatically pass local variables from parent to child scopes.

This is parallelizable for efficient test running, compiles to Python bytecode for efficiency, and follows the same style as doctests.

If you hate DSLs (hey, Python ain’t Ruby!) consider this to be a file-length docstring. If you don’t like doctests then consider this to be a template engine for tests. (Most modern template engines generate Python code…)

# A consultant sits down with a client and hammers out what the client wants
# end-users to be able to do. These are called stories or scenarios.
Scenario: Users can purchase items online.
As a user
I want to spend money
In order to get shiny trinkets
Given we have a user
And the user is logged in
And the user has added some products to the cart
When the user begins checkout
And the user enters shipping details
And the user enters payment details
And the user confirms the order
Then the payment is processed
And product inventory totals are updated
And the order is completed
And the user is notified by e-mail
#!/usr/bin/env feature
# coding: scenario
# This is what the programmer does to the feature file to actually write the tests.
# The style is identical to doctests but with some convenience added in.
# Yes, this is a Python module and is imported like one. Note the coding. :)
# This means your tests are bytecode compiled for efficiency. Line numbers are
# preserved. Imports are fine and would go here.
Scenario: Users can purchase items online.
As a user # This is descriptive text.
I want to spend money # It has no real impact on anything.
In order to get shiny trinkets # (It's a docstring.)
Given we have a user:
>>> user = db.Users.objects.first() # locals() are deep copied and passed down
>>> user
User("Bob Dole", bdole@whitehouse.gov)
And the user is logged in:
>>> web.core.auth.authenticate(user, force=True)
>>> web.core.auth.user == user
True
And the user has added some products to the cart:
>>> products = db.Product.objects[:3]
>>> for product in products:
... api.cart.add(product, 1)
>>> api.cart.items # vvv Abbreviations!
[(1, Product("Ham")), ...]
>>> len(api.cart.items)
3
When the user begins checkout:
>>> transaction = api.cart.transaction.begin()
>>> transaction.state
"incomplete"
>>> transaction.balance
"2.15"
And the user enters shipping details:
>>> transaction.shipping = Address("1 Government Street", "Washington", "DC")
And the user enters payment details:
>>> transaction.method = MockPayment('success', amount="2.15")
And the user confirms the order:
>>> transaction.submit()
Then the payment is processed:
>>> transaction.state
'complete'
>>> transaction.balance
>>> "0.00"
And product inventory totals are updated:
>>> difference = [old - new for old, new in zip(products, db.Product.objects[:3])]
[1, 1, 1]
And the order is completed:
>>> api.cart.empty
True
And the user is notified by e-mail:
>>> log[-1]
"Sending confirmation e-mail to..."
# Scenarios may have data associated with them.
# This data may be in tab, csv, json, or yaml format.
# json data would be a list of dicts, yaml does real record separation
# This data makes the scenario run once for each record.
name: Bob Dole
---
name: Zap Brannigan
@gregglind
Copy link

encoding hack is gross! I have been thinking on this BDD for python issue a lot lately. If Encoding as 'macro preprocessor' catches on, maybe it should get a PEP?

@amcgregor
Copy link
Author

@gregglind

It's less of a hack when you think about how it's implemented. With a template engine like Mako, Jinja2, Genshi, or KID, you're taking a text template with embedded Python instructions (control structures, variable references, and possibly whole blocks of code) and generating a real Python source file from it. Python then process the file and produces an object (module) usually containing a class which is then imported and used by the rendering engine. This usually uses magic hidden inside a wrapper class.

This method simply uses a feature of Python in order to do the translation… which is a class. The end result is the same: Python sees Python code and bytecode compiles it, then it is run.

@amcgregor
Copy link
Author

Here's an example parallel runtime for the above, back-linked for referential integrity: https://gist.github.com/amcgregor/1190561

@amcgregor
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment