- Why Macro Polo? Unit Testing vs Functional Testing
- Requirements
- Installation
- Testing Macro Polo
- Using Macro Polo
- API
- Licensing
Macro Polo is a Python library for unit testing template macros created using popular Python templating systems.
Templating systems/environments currently supported:
Status: Proof of concept
Macro Polo is designed for unit testing template macros. Macro Polo is meant to make it easier to express tests of the resulting HTML of individual template macros (units) within a specific context or with specific inputs. Unit testing macros tests them individually in isolation.
If you're looking at a template and want to test browser-related behavior that occurs when interacting with that template's resulting HTML, you want to investigate functional testing tools and frameworks.
Requirements can be satisfied with pip
:
$ pip install -r requirements.txt
- BeautifulSoup 4 for handling the HTML resulting from template rendering
- Python mock for mocking template filters and context (and unit testing Macro Polo itself)
Template Systems/Environments:
These are not installed by pip
. It is expected that if you are using
these template environments you have them installed already. If not,
their respective environment mixins will
not be available.
To clone and install Macro Polo locally in an existing Python
virtualenv
:
$ git clone https://github.com/cfpb/macropolo
$ pip install -e macropolo
Note: this installs Macro Polo in 'editable' mode. This means that any
updates pulled into the git clone will be 'live' without running pip
again.
Macro Polo can also be installed directly from Github:
$ pip install git+https://github.com/cfpb/macropolo
Macro Polo's own unit tests can be run from the root of the repository:
$ python setup.py test
If you would prefer to use nose:
$ pip install nose
Then you can run the tests:
$ nosetests macropolo
Create a Python file with the rest of your test suite, such as
test_templates.py
. This file will need to define a
base test case,
load template tests from JSON, and (optionally) use Python's
unittest.main()
to run the tests:
from macropolo import MacroTestCase, Jinja2Environment
from macropolo import JSONTestCaseLoader
class MyBaseTestCase(Jinja2Environment, MacroTestCase):
"""
A MacroTestCase subclass for my Jinja2 Templates.
"""
def search_root(self):
"""
Return the root of the search path for templates.
"""
# If the tests live under 'site_root/tests'...
root_dir = os.path.abspath(os.path.join(os.path.dirname( __file__ ),
os.pardir))
return root_dir
def search_exceptions(self):
"""
Return a list of a subdirectory names that should not be searched
for templates.
"""
return ['tests',]
# Create MyTestCase subclasses for all JSON tests and add them to the
# module's global context.
tests_path = os.path.abspath(os.path.join(os.path.dirname( __file__ ), 'template_tests'))
JSONTestCaseLoader(tests_path, MyBaseTestCase, globals())
# Run the tests if we're executed
if __name__ == '__main__':
unittest.main()
Then create your JSON test specifications in
the template_tests
subdirectory of tests
.
Test Case classes should inherit from a
template environment mixin, the
MacroTestCase
class, and should provide the
following methods:
Return the root of the search path for templates.
Return a list of a subdirectory names that should not be searched for templates.
For Example:
from macropolo import MacroTestCase, Jinja2Environment
class MyBaseTestCase(Jinja2Environment, MacroTestCase):
"""
A MacroTestCase subclass for my Jinja2 Templates.
"""
def search_root(self):
"""
Return the root of the search path for templates.
"""
# If the tests live under 'site_root/tests'...
root_dir = os.path.abspath(os.path.join(os.path.dirname( __file__ ),
os.pardir))
return root_dir
def search_exceptions(self):
"""
Return a list of a subdirectory names that should not be searched
for templates.
"""
return ['tests',]
To reduce the amount of boilerplate Python that needs to be written for macro unit tests, unit tests can be written in JSON.
For each template file that defines macros, a single JSON should be created that would look like this:
{
"file": "macros.html",
"tests": [
{
"macro_name": "my_macro",
...
},
{ ... },
]
}
file
is the template file. The test environment uses the same
mechanism that Sheer uses to lookup template files, so the same file
specification that's used within templates that use the macros.
tests
is a list of individual test case specifications. These
corrospond to a single macro.
The specification for a test case for an individual macro looks like this:
{
"macro_name": "<a macro>",
"skip": <true or false>,
"arguments": [ ... ],
"keyword_arguments": { ... },
"context": {
"<context variable>": "<value>",
...
}
"filters": {
"<filter name>": "<mock value>",
"<filter name>": ["<first call mock value>",
"<second call mock value>", ...]
},
"context_functions": {
"<function name>": "<mock value>",
"<function name>": ["<first call mock value>",
"<second call mock value>", ...]
},
"templates": {
"<template file>": {
"<macro name>(<arguments>)": "<mock value>",
"<macro name>()": ["<first call mock value>",
"<second call mock value>", ...]
}
},
"assertions": [
{
"selector": "<css selector>",
"index": <1>,
"value": "<string contained>",
"assertion": "<equal>",
"attribute": "<attribute name>",
"
]
}
macro_name
is simply the name of the macro within the file in which it
is defined.
skip
, if true, will skip the macro test. This is optional.
arguments
is a list of arguments to pass to the macro in the order
they are given. Can be either a list or a JSON object of keys/values; if
it is a JSON object of keys/values, it is presumed to be keyword
arguments. This is optional.
keyword_arguments
is an object containing key/value arguments to pass
to the macro if it requires keyword arguments. This is optional.
context
is an object containing names of context variables to add
to the template's context and values to assign to those variables.
filters
is an object that is used to mock template system filters.
It contains the name of the filter to be mocked and the value that should
be returned when that filter is used. The value can also be a list, in
which case the order of the list will corropsond to the order in which
the filter is called, i.e. if you want the filter to return 1
the
first time it is called, but 2
the second time, the value would be
[1, 2]
. This is optional.
Note: Here are some Sheer filters you may want to consider mocking:
selected_filters_for_field
is_filter_selected
context_functions
is an object that is used to mock template
context functions. It works the same way that filters
does above,
with the values either being a return value for all calls or a list of
return values for each call. This is optional.
Note: Here are some Sheer context functions you may want to consider mocking:
queries
more_like_this
get_document
templates
is an object that is used to mock included template
macros called from within the macro being tested. It works the same
way that filters
and context_functions
do above, with the values
either being a return value for all calls or a list of return values
for each call. The <macro name>
should include parenthesis and any
arguments the template's macro takes. This will override the entire
<template file>
, so make sure to mock all of its macros. This is
optional.
assertions
defines the assertions to make about the result of
rendering the macro. Assertion definitions take a CSS selector
, an
index
in the list of matches for that selector (default is 0
), an
assertion
to make about the selected element or its attribute
(if
given), and a value
for comparison (if necessary for the assertion).
The assertion
can be any of the following:
equal
orequals
not equal
ornot equals
exists
in
not in
Multiple test cases can be defined for the same macro, to test different behavior with different inputs, filter or context funciton output.
If there is a more complex scenario you would like to test that cannot be described by the JSON specification format, you can create a test case in Python.
class MyMacrosTestCase(MyBaseTestCase):
def test_a_macro(self):
self.mock_filter(...)
self.mock_context_function(...)
result = self.render_macro('mymacros.html', 'amacro')
assert 'something' in result.select('.css-selector')[0]
Using the MyBaseTestCase
class defined above
and loading tests defined in JSON files, the
JSON files must be loaded into into a Python context.
For example, in a file called template_tests.py
:
from macropolo import JSONTestCaseLoader
# Create MyTestCase subclasses for all JSON tests and add them to the
# module's global context.
tests_path = os.path.abspath(os.path.join(os.path.dirname( __file__ ), 'template_tests'))
JSONTestCaseLoader(tests_path, MyBaseTestCase, globals())
# Run the tests if we're executed
if __name__ == '__main__':
unittest.main()
From there the file can be run as-is:
$ python template_tests.py
Or it can be run using other Python test runners like nose:
$ pip install nose
$ nosetests
Or py.test
$ pip install pytest
$ py.test
The MacroTestCase
class is intended to capture test cases for
macros on a modular basis, i.e. you would create one subclass of
MacroTestCase
for each template file containing macros. That
subclass can then include test_[macro_name]()
methods that
test each individual macro.
This class requires a
templating system environment mixin
that provides setup_environment()
that creates the templating
system environment, add_filter()
and add_context()
which add
filters and context name/values or functions to the template
environment, and finally render_macro()
which renders the macro
using the template system and environment.
MacroTestCase
provides the following convenience methods:
Mock a template filter. This will create a mock function for the filter that will return either a single value, or will return each of the given values in turn if there are more than one.
Mock a context function. This will create a mock function that will return either a single value, or will return each of the given values in turn if there are more than one.
Make an assertion based on the BeautifulSoup result object.
This method will find the given CSS selector, and make the given assertion about the attribute of selector's match at the given index. If the assertion requires a value to compare to, it should be given. If no attribute is given the assertion is made about the entire match.
Template System environment mixin classes should provide four methods:
This method should setup the templating system's environment.
Render a given macro with the given arguments and keyword arguments. Should return a BeautifulSoup object.
Add the given filter to the template environment.
Add the given name/value to the template environment context.
Load JSON specifications for Jinja2 macro test cases from the given
tests_path
, calls JSONSpecTestCaseFactory()
to create test case
classes with the given super_class
from the JSON files, and adds the
resulting test case classes to the given context
(i.e. globals()
).
Creates a test case class of the given name
with the given
super_class
and mixins
from JSON read from the given json_file
.
The test case class is returned.
Public Domain/CC0 1.0