Examples of pytest, especially funcargs
These are snippets of py.test in action, used in a talk given at | |
PyCon AU 2012 in Hobart, Tasmania. They are all relevant for | |
py.test 2.2 except where specified. Where taken from open source | |
projects I have listed a URL, some examples are from the py.test | |
documentation, some are from my workplace. | |
Apart from things called test_*, these functions should probably | |
be in your conftest.py, although they can generally start life in | |
your test files. | |
Talk info: http://2012.pycon-au.org/schedule/52/view_talk | |
Slides: http://www.slideshare.net/pfctdayelise/funcargs-other-fun-with-pytest | |
http://pytest.org/ | |
http://stackoverflow.com/questions/tagged/py.test | |
http://codespeak.net/mailman/listinfo/py-dev | |
http://lists.idyll.org/listinfo/testing-in-python | |
############## | |
# informative error reporting | |
# tb==native | |
Traceback (most recent call last): | |
File "/workspace/Review/GFESuite/tests/unit/formatters/test_Precis.py", | |
line 882, in test_lowDetail12hourWxBrackets | |
assert 'morningZ' in words | |
AssertionError: assert 'morningZ' in 'morning shower or two' | |
# tb==short | |
tests/unit/formatters/test_Precis.py:882: in test_lowDetail12hourWxBrackets | |
> assert 'morningZ' in words | |
E assert 'morningZ' in 'morning shower or two' | |
# tb==long | |
mockAccessor = | |
def test_lowDetail12hourWxBrackets(mockAccessor): | |
""" | |
Initial 6 hours of wx is correctly being washed out to the first 12 hours. | |
""" | |
mockAccessor.mockGrids([ | |
Flat("Sky", 0, 24, 0), | |
Flat("Wx", 0, 6, "Sct:SH:m::"), | |
Flat("Wx", 6, 24, "NoWx"), | |
]) | |
_icon, words = precisIconWords(mockAccessor, period=6, detail='low') | |
assert 'early' not in words | |
> assert 'morningZ' in words | |
E assert 'morningZ' in 'morning shower or two' | |
tests/unit/formatters/test_Precis.py:882: AssertionError |
# add back in unittest assert statements | |
# apparently cribbed from something in nose | |
def pytest_namespace(): | |
"""Make unittest assert methods available. | |
This is useful for things such floating point checks with assertAlmostEqual. | |
""" | |
import unittest | |
class Dummy(unittest.TestCase): | |
def dummy(self): | |
pass | |
obj = Dummy('dummy') | |
names = {name: member | |
for name, member in inspect.getmembers(obj) | |
if name.startswith('assert') and '_' not in name} | |
return names | |
# test file | |
def test_kn2kmh(): | |
py.test.assertAlmostEqual(UnitConvertor.kn2kmh(10), 18.52, places=4) | |
########################################################### | |
# add a hook for winpdb | |
def pytest_addoption(parser): | |
"""pytest hook that adds a GFE specific option. | |
""" | |
# Add options. | |
group = parser.getgroup('graphical forecast editor options') | |
group.addoption('--winpdb', dest='usewinpdb', action='store_true', default=False, | |
help=('start the WinPDB Python debugger before calling each test function. ' | |
'Suggest only using this with a single test at a time (i.e. -k .')) | |
def pytest_configure(config): | |
# Only do these if this process is the master. | |
if not hasattr(config, 'slaveinput'): | |
# Activate winpdb plugin if appropriate. | |
if config.getvalue("usewinpdb"): | |
config.pluginmanager.register(WinPdbInvoke(), 'winpdb') | |
class WinPdbInvoke: | |
def __init__(self): | |
print "initialising winpdb invoker" | |
def pytest_pyfunc_call(self, pyfuncitem): | |
import rpdb2 | |
rpdb2.start_embedded_debugger('0') | |
# then run: py.test -k test_some_specific_thing --winpdb |
# SKIP | |
# inside a test, if you need to check something after the environment | |
# has been loaded | |
if not config.get('ifpServer.allowOfficalWrites'): | |
py.test.skip('Official DB writes are not allowed.') | |
# decorators: | |
import sys | |
win32only = pytest.mark.skipif("sys.platform != 'win32'") | |
@win32only | |
def test_foo(): | |
.... | |
@py.test.mark.skipif('True') | |
def test_foo1(): | |
print "foo1" | |
@py.test.mark.skipif('False') | |
def test_foo2(): | |
print "foo2" | |
def test_foo3(): | |
py.test.skip('inside skip') | |
print "foo3" | |
# XFAIL | |
@py.test.mark.xfail | |
def test_foo4(): | |
assert False | |
@py.test.mark.xfail(reason='This is a bad idea') | |
def test_foo5(): | |
assert False | |
@py.test.mark.xfail(reason='Maybe this was a bad idea once') | |
def test_foo6(): | |
assert True | |
def test_foo7(): | |
# force test to be recorded as an xfail, | |
# even if it would otherwise pass | |
py.test.xfail() | |
assert True | |
# output: | |
test_foo4 xfail | |
test_foo5 xfail | |
test_foo6 XPASS | |
test_foo7 xfail | |
# with --runxfail: | |
test_foo4 FAILED | |
test_foo5 FAILED | |
test_foo6 PASSED | |
test_foo7 FAILED | |
# plus tracebacks | |
# example custom marks | |
@py.test.mark.slow | |
@py.test.mark.dstAffected | |
@py.test.mark.mantis1543 | |
@py.test.mark.flaky | |
@py.test.mark.unicode | |
@py.test.mark.regression | |
# in 2.2 - parametrize | |
@pytest.mark.parametrize(("input", "expected"), [ | |
("3+5", 8), | |
("2+4", 6), | |
("6*9", 42), | |
]) | |
def test_eval(input, expected): | |
assert eval(input) == expected |
# code | |
def isSquare(n): | |
n = n ** 0.5 | |
return int(n) == n | |
# test file | |
def pytest_generate_tests(metafunc): | |
squares = [1, 4, 9, 16, 25, 36, 49] | |
for n in range(1, 50): | |
expected = n in squares | |
if metafunc.function.__name__ == 'test_isSquare': | |
metafunc.addcall(id=n, funcargs=dict(n=n, expected=expected)) | |
def test_isSquare(n, expected): | |
assert isSquare(n) == expected | |
# then: | |
# conftest.py | |
def pytest_generate_tests(__multicall__, metafunc): | |
"""Supports parametrised tests using generate_ fns. | |
Use multicall to call any other pytest_generate_tests hooks first. | |
If the test_ fn has a generate_ fn then call it with the metafunc | |
to let it parametrise the test. | |
""" | |
__multicall__.execute() | |
name = metafunc.function.__name__.replace('test_', 'generate_') | |
fn = getattr(metafunc.module, name, None) | |
if fn: | |
fn(metafunc) | |
# generate function is simplified, no boilerplate! | |
# and we can have one per test function with multiple pairs in a single module, woot! | |
def generate_isSquare(metafunc): | |
squares = [1, 4, 9, 16, 25, 36, 49] | |
for n in range(1, 50): | |
expected = n in squares | |
metafunc.addcall(id=n, funcargs=dict(n=n, expected=expected)) |
import os.path | |
def getssh(): # pseudo application code | |
return os.path.join(os.path.expanduser("~admin"), '.ssh') | |
def test_getssh(monkeypatch): | |
def mockreturn(path): | |
return '/abc' | |
monkeypatch.setattr(os.path, 'expanduser', mockreturn) | |
x = getssh() | |
assert x == '/abc/.ssh' | |
###################### | |
# a funcarg to hide/abstract away some monkeypatching | |
def pytest_funcarg__noPreviousWarnings(request): | |
"""pytest funcarg to avoid retrieving REAL previously issued warnings""" | |
_ = request.getfuncargvalue("textImporter") | |
def setup(): | |
import RecognisedWarnings as RW | |
monkeypatch = request.getfuncargvalue("monkeypatch") | |
noPreviousWarnings = lambda _x, _y, _z: None | |
monkeypatch.setattr(RW, '_getFDBViewerXML', noPreviousWarnings) | |
def teardown(obj): | |
pass | |
return request.cached_setup(setup, teardown, scope='function') |
# http://anders.conbere.org/blog/2011/05/03/setup_and_teardown_methods_with_py.test/ | |
# conftest.py | |
def setup_fixtures(): | |
db.insert('...') | |
return db | |
def teardown_fixtures(db): | |
db.destroy('..') | |
def py_test_funcarg__db(request): | |
return request.cached_setup( | |
setup = setup_fixtures, | |
teardown = teardown_fixtures, | |
scope = "module") | |
# test_db.py | |
def test_db(db): | |
assert(len(db.query(x=y)) >= 1) | |
##################### | |
# a real DB example | |
# still far from a good example for most use cases I suspect, what with the lack of ORM and all | |
def setupTestDb(): | |
"""Setup test gfedb ensuring we only do this once.""" | |
import gfeDB | |
if gfeDB.DB_NAME == 'gfedb': | |
from NeoConfig import config as neoConfig | |
name = 'gfedbtest{}'.format(neoConfig['instance.port']) | |
cmd = """\ | |
mysql -u root -e "DROP DATABASE IF EXISTS {name}; | |
CREATE DATABASE {name}; | |
USE {name}; | |
GRANT ALL ON * TO gfe; | |
GRANT ALL ON * TO gfe@localhost; | |
FLUSH PRIVILEGES;"; | |
mysqldump -u root --no-data gfedb | mysql -u root {name} | |
""".format(name=name) | |
subprocess.check_output(cmd, shell=True) | |
gfeDB.DB_NAME = name | |
def pytest_funcarg__testDb(request): | |
"""pytest funcarg for a test gfedb.""" | |
return request.cached_setup(setup=setupTestDb, | |
scope='session') | |
def pytest_funcarg__emptyDb(request): | |
"""pytest funcarg to truncate all tables in the test gfedb.""" | |
_ = request.getfuncargvalue('testDb') | |
def setup(): | |
from NeoConfig import config as neoConfig | |
name = 'gfedbtest{}'.format(neoConfig['instance.port']) | |
cmd = """\ | |
mysql -u root -e "USE {name}; | |
DELETE FROM fire_event_tb; | |
DELETE FROM forecast_tb; | |
DELETE FROM issuance_tb; | |
DELETE FROM precis_pop_areas_tb; | |
DELETE FROM warning_tb;" | |
""".format(name=name) | |
subprocess.check_output(cmd, shell=True) | |
return request.cached_setup(setup=setup, | |
scope='function') | |
########################### | |
# funcarg to express a prerequisite | |
# https://github.com/lunaryorn/pyudev/blob/develop/tests/test_libudev.py | |
def pytest_funcarg__libudev(request): | |
try: | |
return _libudev.load_udev_library() | |
except ImportError: | |
pytest.skip('udev not available') |
# django example | |
# http://pytest-django.readthedocs.org/en/latest/helpers.html | |
from myapp.views import my_view | |
def test_details(rf): | |
request = rf.get('/customer/details') | |
response = my_view(request) | |
assert response.status_code == 200 | |
def test_an_admin_view(admin_client): | |
response = admin_client.get('/admin/') | |
assert response.status_code == 200 | |
############################## | |
# Google App Engine | |
# I wonder if these examples should be using monkeypatch to do os.environ.update?? | |
# http://pypi.python.org/pypi/pytest_gae/0.2.1 | |
import os | |
from webtest import TestApp | |
from main import make_application | |
def pytest_funcarg__anon_app(request): | |
os.environ.update({'USER_EMAIL': '', | |
'USER_ID': '', | |
'AUTH_DOMAIN': 'google', | |
'USER_IS_ADMIN': '0'}) | |
return TestApp(make_application()) | |
def pytest_funcarg__user_app(request): | |
os.environ.update({'USER_EMAIL': 'simple@google.com', | |
'USER_ID': '1', | |
'AUTH_DOMAIN': 'google', | |
'USER_IS_ADMIN': '0'}) | |
return TestApp(make_application()) | |
def pytest_funcarg__admin_app(request): | |
os.environ.update({'USER_EMAIL': 'admin@google.com', | |
'USER_ID': '2', | |
'AUTH_DOMAIN': 'google', | |
'USER_IS_ADMIN': '1'}) | |
return TestApp(make_application()) | |
def test_index(anon_app): | |
assert "Index" in anon_app.get('/index') | |
def test_user_with_user(user_app): | |
assert "User" in user_app.get('/users') | |
def test_user_with_anon(anon_app): | |
assert '302 Moved Temporarily' == anon_app.get('/users').status | |
def test_user_with_admin(admin_app): | |
assert "Admin" in admin_app.get('/users') |
# https://bitbucket.org/hpk42/py/src/0fca612a4bbd/conftest.py: | |
def pytest_generate_tests(metafunc): | |
multi = getattr(metafunc.function, 'multi', None) | |
if multi is not None: | |
assert len(multi.kwargs) == 1 | |
for name, l in multi.kwargs.items(): | |
for val in l: | |
metafunc.addcall(funcargs={name: val}) | |
elif 'anypython' in metafunc.funcargnames: | |
for name in ('python2.4', 'python2.5', 'python2.6', | |
'python2.7', 'python3.1', 'pypy-c', 'jython'): | |
metafunc.addcall(id=name, param=name) | |
def pytest_funcarg__anypython(request): | |
name = request.param | |
executable = getexecutable(name) | |
if executable is None: | |
if sys.platform == "win32": | |
executable = winpymap.get(name, None) | |
if executable: | |
executable = py.path.local(executable) | |
if executable.check(): | |
return executable | |
py.test.skip("no %s found" % (name,)) | |
return executable |
#probable in 2.3 (not yet released) | |
# content of conftest.py | |
import pytest | |
import smtplib | |
@pytest.factory(scope="session", | |
params=["merlinux.eu", "mail.python.org"]) | |
def smtp(testcontext): | |
smtp = smtplib.SMTP(testcontext.param) | |
def fin(): | |
print ("finalizing %s" % smtp) | |
smtp.close() | |
testcontext.addfinalizer(fin) | |
return smtp | |
# content of test_module.py | |
def test_ehlo(smtp): | |
response = smtp.ehlo() | |
assert response[0] == 250 | |
assert "merlinux" in response[1] | |
assert 0 # for demo purposes | |
def test_noop(smtp): | |
response = smtp.noop() | |
assert response[0] == 250 | |
assert 0 # for demo purposes | |
$ py.test --collectonly | |
=========================== test session starts ============================ | |
platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 | |
plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov | |
collecting ... collected 4 items | |
<Module 'test_module.py'> | |
<Function 'test_ehlo[merlinux.eu]'> | |
<Function 'test_noop[merlinux.eu]'> | |
<Function 'test_ehlo[mail.python.org]'> | |
<Function 'test_noop[mail.python.org]'> | |
============================= in 0.02 seconds ============================= |
# v0 | |
# Feb 2010 | |
# data/test/dbconfig/TEXT/Misc/District_TestScript_2.py | |
{ | |
"name": "Thunderstorms with heavy rain are not dry", | |
"commentary": "Mantis 01530", | |
"productType": "District", | |
"createGrids": [ | |
("Fcst", "Wx", "WEATHER", 0, 24, "Chc:TS:!::r", "all"), | |
], | |
"notCheckStrings": [ | |
"dry" | |
], | |
"fileChanges": [ | |
("District_NSWRO_Definition", "TextUtility", "add", defaultEditAreas, "undo"), | |
], | |
"cmdLineVars": str({ | |
('Generate Days', 'productIssuance'): 'All Periods', | |
('Issuance Type', 'issuanceType'): 'Morning', | |
('Issued By', 'issuedBy'): None, | |
('CompleteUpdate', 'completeUpdate'): 'yes'}), | |
}, | |
# v1 | |
# early 2011 - move to pytest, directory restructure | |
def test_Thunderstorms_with_heavy_rain_are_not_dry(formatterTester): | |
formatterTester.run({ | |
"commentary": "Mantis 01530", | |
"productType": "District", | |
"createGrids": [ | |
Flat("Wx", 0, 24, "Chc:TS:!::r"), | |
], | |
"notCheckStrings": [ | |
"dry" | |
], | |
"cmdLineVars": cmdLineVarsIssuanceMorning | |
}, defaults()) | |
# v2 | |
# feb 2011 - introduction of gridCreator | |
def test_Thunderstorms_with_heavy_rain_are_not_dry(formatterTester, gridCreator): | |
"""Mantis 01530 | |
""" | |
gridCreator.createGrids([ | |
Flat("Wx", 0, 24, "Chc:TS:!::r"), | |
]) | |
formatterTester.run({ | |
"productType": "District", | |
"notCheckStrings": [ | |
"dry" | |
], | |
"cmdLineVars": cmdLineVarsIssuanceMorning | |
}, defaults()) | |
# lots of specifying areas like this: | |
siteID = siteConfig.GFESUITE_SITEID | |
DistrictEditAreaDictionary = { | |
"NSWRO" : ['NSW_CW013'], | |
"VICRO" : ['VIC_CW010', 'VIC_CW011'], | |
"QLDRO" : [], | |
"SARO" : [], | |
"TASRO" : [], | |
"WARO" : [], | |
"NTRO" : [], | |
} | |
area = DistrictEditAreaDictionary[siteID] | |
# v3 | |
# March 2011 | |
def pytest_funcarg__districtArea(request): | |
def start(): | |
areas = { | |
"VICRO": DefaultEditArea("VIC_PW007", "Central"), | |
"NSWRO": DefaultEditArea("NSW_PW014", "Riverrina"), | |
"TASRO": DefaultEditArea("TAS_PW002", "North East"), | |
"SARO": DefaultEditArea("SA_PW012", "North West Pastoral"), | |
} | |
return __areaStart(areas) | |
return request.cached_setup(setup=start, scope='session') | |
def test_Thunderstorms_with_heavy_rain_are_not_dry(formatterTester, gridCreator, districtArea): | |
"""Mantis 01530 | |
""" | |
gridCreator.createGrids([ | |
Flat("Wx", 0, 24, "Chc:TS:!::r"), | |
]) | |
formatterTester.run({ | |
"productType": "District", | |
"notCheckStrings": [ | |
"dry" | |
], | |
"cmdLineVars": cmdLineVarsIssuanceMorning(districtArea.aac) | |
}, defaults(districtArea.aac)) | |
# v4 | |
# Nov 2011 | |
# tests/system/formatters/wxPhrase/test_attributes.py | |
@py.test.mark.mantis1530 | |
def test_thunderstormsWithHeavyRainAreNotDry(mockAccessor): | |
"""'heavy rain' and 'dry' attributes were getting mixed up. | |
""" | |
mockAccessor.mockGrids([ | |
Flat("Wx", 0, 24, "Chc:TS:!::r"), | |
]) | |
words = weatherWordsLandAreal(mockAccessor) | |
assert "dry" not in words |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment