Skip to content

Instantly share code, notes, and snippets.

@pfctdayelise
Created August 18, 2012 13:47
Show Gist options
  • Save pfctdayelise/3386951 to your computer and use it in GitHub Desktop.
Save pfctdayelise/3386951 to your computer and use it in GitHub Desktop.
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': '[email protected]',
'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': '[email protected]',
'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