To reduce boiler plate code i came upon the idea to generate test cases in the class Tester for all parameter-less methods.
On running py.test, it only recognizes the statically written test cases (test_a, test_b) but not the dynamically created test cases using setattr(Tester,'test_' + name, member)
Perhaps py.test has already inspected the class Tester for methods with 'test_*' before setUpClass is called? Any hints how to get this running?
import inspect
import unittest
class Testee:
def a(self):
print('a')
def b(self):
print('b')
#...
#...
def z(self):
print('z')
class Tester(unittest.TestCase):
#classmethod
def setUpClass(cls):
testee = Testee()
for name, member in inspect.getmembers(object=testee, predicate=inspect.ismethod or inspect.iscoroutine):
if len(inspect.signature(member).parameters):
print(str(inspect.signature(member).parameters))
setattr(Tester,'test_' + name, member)
if inspect.isfunction(member) or inspect.ismethod(member):
setattr(Tester,'test_' + name, member)
elif inspect.iscoroutinefunction(member):
setattr(Tester,'test_' + name, functools.partialmethod(TestInstrument.run_coro, member))
else:
print(member)
return super().setUpClass()
def test_a(self):
Tester.testee.a()
def test_b(self):
Tester.testee.b()
============================= test session starts ============================= platform win32 -- Python 3.5.1, pytest-2.9.2, py-1.4.31, pluggy-0.3.1 -- c:\program
files\python35\python.exe cachedir: .cache rootdir: C:\tests, inifile:
collected 2 items
sandbox.py::Tester::test_a PASSED sandbox.py::Tester::test_b PASSED
========================== 2 passed in 0.03 seconds ===========================
EDIT: If i move the code in setupClass to the global scope (outside the class), then py.test detects and runs the auto-generated test cases.
To elaborate on my 'EDIT', one option would be so. I'm unhappy with the solution because global code execution would be open to side-effects and other subtle bugs. any suggestions on how to get this into class Tester scope?
import inspect
import unittest
import functools
def auto_generate_test_cases(Tester, Testee):
def run(self, fn):
fn(Tester._testee)
for name, member in inspect.getmembers(
object=Testee, predicate=inspect.isfunction or inspect.iscoroutine):
if len(inspect.signature(member).parameters) == 1:
setattr(Tester,'test_' + name, functools.partialmethod(run, member))
class Testee:
def __init__(self):
self._a = 'a'
def a(self):
print(self._a)
def ab(self, a, b):
print('a')
def b(self):
print('b')
def h(self):
print('h')
async def q(self):
print('async q')
#...
#...
def z(self):
print('z')
class Tester(unittest.TestCase):
_testee = Testee()
auto_generate_test_cases(Tester, Testee)
py.test. output:
C:\tests>py.test sandbox.py --verbose
============================= test session starts =============================
platform win32 -- Python 3.5.1, pytest-2.9.2, py-1.4.31, pluggy-0.3.1 -- c:\program files\python35\python.exe
cachedir: .cache
rootdir: C:\\tests, inifile:
collected 5 items
sandbox.py::Tester::test_a PASSED
sandbox.py::Tester::test_b PASSED
sandbox.py::Tester::test_h PASSED
sandbox.py::Tester::test_q PASSED
sandbox.py::Tester::test_z PASSED
========================== 5 passed in 0.07 seconds ===========================
Related
I want to test a class method with py.test and I've followed this post Testing class methods with pytest.
I've got a file with my method (calgo.py) and another file with my test (test_calgo.py), and both files are in the same folder (learning_ut).
When I run pytest I get an error message saying fixture 'self' not found.
Where am I going wrong?
Thanks!
# calgo.py
class MyClass():
def func(self, x):
return x+1
# test_calgo.py
import calgo
def test_func(self):
mc = MyClass()
assert mc.func(3) == 4
# Command line
(pyenv) C:\Users\Jimmy\Desktop\learning_ut>pytest
# Error message
(pyenv) C:\Users\Jimmy\Desktop\learning_ut>pytest
================================================= test session starts =================================================
platform win32 -- Python 3.6.13, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: C:\Users\Jimmy\Desktop\learning_ut
collected 1 item
test_calgo.py E [100%]
======================================================= ERRORS ========================================================
_____________________________________________ ERROR at setup of test_func _____________________________________________
file C:\Users\Jimmy\Desktop\learning_ut\test_calgo.py, line 11
def test_func(self):
E fixture 'self' not found
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary,
doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property,
record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
> use 'pytest --fixtures [testpath]' for help on them.
C:\Users\Jimmy\Desktop\learning_ut\test_calgo.py:11
=============================================== short test summary info ===============================================
ERROR test_calgo.py::test_func
I don't know about pytest, in unittest you create a test class, which would have method belonging to this class and have self as parameter. However, your error seems to be causes by the function in calgo.py, which does not belong to a class, thus does not have the self reference. Try the following:
import calgo
def test_func():
mc = MyClass()
assert mc.func(3) == 4
I am trying to run pytest from within python after running a thread,
here is a simple example of the case:
import time
import threading
import pytest
class A(object):
def __init__(self):
self._thread_a = threading.Thread(target=self.do_a)
self._thread_a.start()
pytest.main()
def do_a(self):
print "a"
time.sleep(2)
self.do_a()
if __name__ == "__main__":
a = A()
but pytest keeps hanging. this is what the output looks like:
============================= test session starts ==============================
platform darwin -- Python 2.7.10, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
metadata: {'Python': '2.7.10', 'Platform': 'Darwin-17.3.0-x86_64-i386-64bit', 'Packages': {'py': '1.5.2', 'pytest': '3.3.2', 'pluggy': '0.6.0'}, 'Plugins': {'session2file': '0.1.9', 'celery': '4.0.0','html': '1.16.1', 'metadata': '1.5.1'}}
rootdir: /Users/path/to/root/dir, inifile:
plugins: session2file-0.1.9, metadata-1.5.1, html-1.16.1, celery-4.0.0
and it just hangs like this until I force quit it.
is there any way to make this work?
Two things to mention:
you possibly want to stop the thread if you code is done with something.
you may also separate your code from the test code.
Lets say:
file.py
import time
import threading
class A(object):
def __init__(self):
self._thread_a = threading.Thread(target=self.do_a)
self._thread_a.daemon = True
self._thread_a.start()
def do_a(self):
print "a"
time.sleep(2)
self.do_a()
def stop(self, timeout):
self._thread_a.join(timeout)
test_file.py
import pytest
from .file import A
#pytest.fixture()
def a():
return A()
def test_sth(a):
a.start()
print('I can do sth else')
a.stop(timeout=1)
I have a below code in my conftest.py
import pytest
#pytest.fixture(scope="class")
def fix1():
print("i am in fix1")
a = 10
return a
#pytest.fixture(scope="class")
def fix2():
print("i am in fix2")
b = 20
return b
#pytest.fixture(scope="session",autouse=True)
def setup_session(request):
tp = TestSetup(fix1)
tp.setup()
def teardown_session():
tp.teardown()
request.addfinalizer(teardown_session)
class TestSetup(object):
def __init__(self, fix1, fix2):
self.fix1 = fix1
self.fix2 = fix2
def setup(self):
print("i am in setup")
print(self.fix1)
def teardown(self):
print("I am in teardown")
print(self.fix2)
# py.test -s test1.py
=========== test session starts ===========
platform linux2 -- Python 2.7.5, pytest-2.8.5, py-1.4.31, pluggy-0.3.1
rootdir: /tmp/test, inifile:
collected 2 items
test1.py i am in setup
<function fix1 at 0x2948320>
i am in test1
.i am in test2
.I am in teardown
when i execute the above using pytest, the fixtures fix1 and fix2 are never called. I need a way to call fix1 and fix2 as part of of setup and teardown before any of my tests runs.
What i am a trying to achieve is before any of my test runs, i need to have a setup created, the fix1, and fix2 are fixture which setup few things . I would like to have these fixture be called before any of my test runs and once all the tests are run , i call teardown function to teardown my setup.
If what you want it to create a set of fixtures that get created once per session, reused for every test and then torn down, the py.test way of doing that would be:
import pytest
#pytest.fixture(scope="session")
def fix1(request):
# setup code here
print('creating fix1')
a = 10
def teardown_fix1():
# teardown code here
print('destroying fix1')
a = None
request.addfinalizer(teardown_fix1)
return a
def testOne(fix1):
print('in testOne')
assert fix1 == 10
def testTwo(fix1):
print('in testTwo')
assert fix1 != 20
(You already know how to do this, as shown in your code, so nothing new here).
You could have several of these session scoped fixtures, and the fact that they are session scoped guarantees that they will be created once, and then torn down at the end of the test run.
There is no need to have one master setup function that will set up every fixture and another which will tear them down, rather the idea is that you separate these into their own little factory functions with a finalizer as shown.
EDIT
But maybe there are a lot of fixtures that look pretty much the same, and you want to reuse some code. In that case, you could have a class that manages the fixtures and make that a pytest fixture. Then you declare other pytest fixtures that depend on the manager, each returning a specific fixture:
import pytest
class FixtureManager:
def __init__(self):
self.fixtures = {}
def setup(self):
print('setup')
self.fixtures['fix1'] = self.createFixture('fix1')
self.fixtures['fix2'] = self.createFixture('fix2')
self.fixtures['fix3'] = self.createFixture('fix3')
def get(self, name):
return self.fixtures[name]
def teardown(self):
print('teardown')
for name in self.fixtures.keys():
# whatever you need to do to delete it
del self.fixtures[name]
def createFixture(self, name):
# whatever code you do to create it
return 'Fixture with name %s' % name
#pytest.fixture(scope="session")
def fixman(request):
fixman = FixtureManager()
fixman.setup()
request.addfinalizer(fixman.teardown)
return fixman
#pytest.fixture
def fix1(fixman):
return fixman.get('fix1')
#pytest.fixture
def fix2(fixman):
return fixman.get('fix2')
def testOne(fix1):
print('in testOne')
assert fix1 == 'Fixture with name fix1'
def testTwo(fix2):
print('in testTwo')
assert fix2 == 'Fixture with name fix2'
Of course, you could do away with creating the fix1, fix2 pytest fixtures, and get at those instead by injecting fixman into your test functions and calling get there. You be the judge what makes more sense and generates least boilerplate.
I have a below py.test program where i need to 2 fixtures, one with scope "session" and other with scope "class", the fixture with scope "class" takes fixture with "session" as one of it's argument.
While running tests that uses scope as "class", the tests seems to run twice,
Below is the code.
import pytest
#pytest.fixture(scope="session")
def session_fixture(request):
data = ['hello world']
return data
#pytest.fixture(scope="class")
def class_fixture(session_fixture, request):
if hasattr(request.cls, 'test1'):
request.cls().test1(session_fixture)
return session_fixture
class TestClass:
def test1(self,class_fixture):
print("Hello World")
When i run the test, it seems to print "hello world" twice.
Ouput:
$ py.test test7.py -s
=============== test session start============================
platform linux2 -- Python 2.7.5 -- py-1.4.27 -- pytest-2.7.0
rootdir: /root/fix2, inifile:
plugins: multihost
collected 1 items
test7.py Hello World
Hello World
.
================ 1 passed in 0.09 seconds ===================
In the above program if i use the fixture "session_fixture" directly instead of "class_fixture", i see that "Hello world" is printed only once.
Any hints on how i could resolve the issue.
Your 'test' case seems to be incorrect
import pytest
#pytest.fixture(scope="session")
def session_fixture(request):
data = ['hello world']
print("Session fixture")
return data
#pytest.fixture(scope="class")
def class_fixture(session_fixture, request):
print("Class fixture")
return session_fixture
class TestClass:
def test1(self,class_fixture):
print("Hello World 1")
def test2(self, class_fixture):
print('Hello World 2')
this gives:
py.test test.py -s
collected 2 items
test.py::TestClass::test1 Session fixture
Class fixture
Hello World 1
PASSED
test.py::TestClass::test2 Hello World 2
PASSED
So class-based fixture is executed only once.
Pytest has massive test coverage on simple cases like this to be sure it always works as declared.
I am able to setup nose tests to run with the #attr tag. I am now interested in know if I can append to the end of the test name, the #attr tag? What we are trying to do is add a tag if our tests run into an issue and we write up a defect for it, we would then put the defect number as an #attr tag. Then when we run we could easily identify which tests have open defects against them.
Just wondering if this is even possible, and where to go to see how to set it up?
EDIT RESULTS RUNNING WITH ANSWER:
Test Results:
So I sort of know what is going on, if I have the #fancyattr() at the class level it picks it up and changes the name of the class. When I put the #fancyattr() at the test level it is not changing the name of the test, which is what I need for it to do.
For example - Changes the name of the class:
#dms_attr('DMSTEST')
#attr('smoke_login', 'smoketest', priority=1)
class TestLogins(BaseSmoke):
"""
Just logs into the system and then logs off
"""
def setUp(self):
BaseSmoke.setUp(self)
def test_login(self):
print u"I can login -- taking a nap now"
sleep(5)
print u"Getting off now"
def tearDown(self):
BaseSmoke.tearDown(self)
This is what I need and it isn't working:
#attr('smoke_login', 'smoketest', priority=1)
class TestLogins(BaseSmoke):
"""
Just logs into the system and then logs off
"""
def setUp(self):
BaseSmoke.setUp(self)
#dms_attr('DMSTEST')
def test_login(self):
print u"I can login -- taking a nap now"
sleep(5)
print u"Getting off now"
def tearDown(self):
BaseSmoke.tearDown(self)
Updated screenshot with what I am seeing with __doc__:
Here is how to do it with args type attributes:
rename_test.py:
import unittest
from nose.tools import set_trace
def fancy_attr(*args, **kwargs):
"""Decorator that adds attributes to classes or functions
for use with the Attribute (-a) plugin. It also renames functions!
"""
def wrap_ob(ob):
for name in args:
setattr(ob, name, True)
#using __doc__ instead of __name__ works for class methods tests
ob.__doc__ = '_'.join([ob.__name__, name])
#ob.__name__ = '_'.join([ob.__name__, name])
return ob
return wrap_ob
class TestLogins(unittest.TestCase):
#fancy_attr('slow')
def test_method():
assert True
#fancy_attr('slow')
def test_func():
assert True
Running test:
$ nosetests rename_test.py -v
test_method_slow ... ok
test_func_slow ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.003s
OK
EDIT: For xunit reporting to work, test renaming should take place before running the test. You can do it on import, here is untested hack showing how to do it:
rename_test.py:
import unittest
def fancy_attr(*args, **kwargs):
"""Decorator that adds attributes to classes or functions
for use with the Attribute (-a) plugin. It also renames functions!
"""
def wrap_ob(ob):
for name in args:
setattr(ob, name, True)
ob.__doc__ = '_'.join([ob.__name__, name])
return ob
return wrap_ob
class TestLogins(unittest.TestCase):
#fancy_attr('slow')
def test_method(self):
assert True
def make_name(orig, attrib):
return '_'.join([orig, attrib])
def rename(cls):
methods = []
for key in cls.__dict__:
method = getattr(cls, key)
if method:
if hasattr(cls.__dict__[key], '__dict__'):
if 'slow' in cls.__dict__[key].__dict__:
methods.append(key)
print methods
for method in methods:
setattr(cls, make_name(method, 'slow'), cls.__dict__[key])
delattr(cls, method)
rename(TestLogins)
#fancy_attr('slow')
def test_func():
assert True