I need to create a class that uses a fixture from conftest.py, and this class has a fixture that I need to use only once per test session.
I have two test classes that depend on this class that has a fixture.
Sample code in test_app.py:
#pytest.mark.usefixtures("driver_get")
class TestBase:
#pytest.fixture(scope="module", autouse=True)
def set_up(self):
# set up code.
# web page will load if this fixture is called
# uses self.driver, where driver was set in driver_get
class TestOne(TestBase):
def test_1(self):
# test code
# uses self.driver also in the test
class TestTwo(TestBase):
def test_2(self):
# test code
# uses self.driver also in the test
Sample code in conftest.py (follows https://dzone.com/articles/improve-your-selenium-webdriver-tests-with-pytest):
#pytest.fixture(scope="session")
def driver_get(request):
from selenium import webdriver
web_driver = webdriver.Chrome()
session = request.node
for item in session.items:
cls = item.getparent(pytest.Class)
setattr(cls.obj,"driver",web_driver)
yield
web_driver.close()
As you can see, conftest.py sets the driver as a class attribute, which is why I am applying the driver_get fixture to class TestBase, since I need to use the driver inside the class.
The problem is, once TestOne finishes, the web page will load again, and execute TestTwo, which means that the fixture set_up was executed again, which is not what I want (since I set set_up scope to module, so it should only really happen once).
I know there is a similar question asked here (py.test method to be executed only once per run), but the asker didn't have the constraint of needing TestBase to have a fixture applied to it as well.
I have thought of putting the fixture inside conftest.py, but I am not sure if it is possible given my constraint of the fixture needing to be inside a class, and executed only once.
Any help will be appreciated. Thank you!
In your code, your set_up fixture which is module scope is called before your TestBase resolves the driver_get fixture. Because of this, at set_up trying self.driver will raise an AttributeError: object has no attribute 'driver'.
One quick way to fix this in your example code is to refer to the driver_get fixture in your module set_up fixture like so:
class TestBase:
#pytest.fixture(scope="module", autouse=True)
def set_up(self, driver_get): # <-------------
self.driver.get("https://www.yahoo.com")
Another way to refer to fixtures is just by including the fixture name as an argument.
Personally I am not a huge fan of the approach you copied from that blog of setting a class attribute on the request node. You'd get an IDE warning about referring to self.driver. To me, it would be clearer to yield the driver out of driver_get and then within the test classes either set it to self in a setup style fixture or use it directly. Similar to something below.
#pytest.fixture(scope="session")
def driver_get():
from selenium import webdriver
web_driver = webdriver.Chrome()
yield web_driver
web_driver.close()
class TestClass:
#pytest.fixture(autouse=True)
def setup(self, driver_get):
self.driver = driver_get
def test_something(self):
self.driver.get("https://www.google.com")
But depending on what else needs to be setup, if you want to control it happening just once per session or module you would need to modify the approach a bit.
Related
I am using this class which creates my login test:
import pytest
from pages.loginPage import LoginPage
from utils import utilis as utils
#pytest.mark.usefixtures("test_setup")
class TestLogin():
def test_login(self):
driver=self.driver
driver.get(utils.URL)
login =LoginPage(driver)
login.enterUsername(utils.USERNAME)
login.enterPassword(utils.PASSWORD)
login.clickLogin()
I want to re-use this test as a fixture for other tests, like this:
import pytest
from pages.loginPage import LoginPage
from pages.homePage import HomePage
from utils import utilis as util
#pytest.mark.usefixtures("test_login")
class TestAddRegulation():
def test_addRegulation(self):
driver = self.driver
homepage = HomePage(driver)
homepage.clickRegulationTile()
homepage.clickAddRegulationListItem()
And this is the conftest.py file with the test_setup fixture:
from selenium import webdriver
import pytest
def pytest_addoption(parser):
parser.addoption("--browser", action="store",
default="chrome",
help="Type in browser name e.g.chrome OR firefox")
#pytest.fixture(scope="class")
def test_setup(request):
browser = request.config.getoption("--browser")
if browser == 'chrome':
driver = webdriver.Chrome(executable_path=
r"C:/Users/user/PycharmProjects/RCM_AutomationFramework/drivers/chromedriver.exe")
elif browser == 'firefox':
driver = webdriver.Firefox(executable_path=
r"C:/Users/user/PycharmProjects/RCM_AutomationFramework/drivers/geckodriver.exe")
driver.implicitly_wait(5)
driver.maximize_window()
request.cls.driver = driver
yield
driver.close()
driver.quit()
print("Test is finished")
I can't get this to work, even if the test_login case is executed before the test_addRegulation test case.
I tried marking test_login as a fixture but it doesn't work. I can make it work if I dropped using classes.
Can I make a class method a fixture that is re-usable for other test classes?
Fixtures can be methods defined in a class, but then they are not available outside of the class. As the pytest documentation on fixtures states:
Fixture availability is determined from the perspective of the test. A fixture is only available for tests to request if they are in the scope that fixture is defined in. If a fixture is defined inside a class, it can only be requested by tests inside that class.
(Bold emphasis mine).
This means that you have to use a plain function to define re-usable fixtures. You can still access the class used by each test, however, via the request.cls attribute. Make sure to have the fixture take both the request and the test_setup scopes:
#pytest.fixture(scope="class")
def login(request, test_setup):
driver = request.cls.driver
driver.get(utils.URL)
login = LoginPage(driver)
login.enterUsername(utils.USERNAME)
login.enterPassword(utils.PASSWORD)
login.clickLogin()
Just put that fixture in your conftest.py file. You can use a different scope, provided it doesn't exceed the class scope of the test_setup fixture (so your choices are class and function here).
You can then use that fixture with no actual test body to test the login:
#pytest.mark.usefixtures("login")
class TestLogin:
def test_login(self):
# test passes if the login fixture completes.
pass
This does seem a bit redundant, of course.
Use the fixture for other classes the same way:
#pytest.mark.usefixtures("login")
class TestAddRegulation:
def test_addRegulation(self):
# ... etc.
A quick demo (without selenium, just plain Python):
import pytest
#pytest.fixture(scope="class")
def test_setup(request):
request.cls.fixtures = ["test_setup"]
yield
print("Test is finished, fixtures used:", request.cls.fixtures)
#pytest.fixture(scope="class")
def login(request, test_setup):
# The fixtures list was created by the test_setup fixture
fixtures = request.cls.fixtures
fixtures.append("login")
#pytest.mark.usefixtures("login")
class TestLogin:
def test_login(self):
assert self.fixtures == ["test_setup", "login"]
#pytest.mark.usefixtures("login")
class TestAddRegulation:
def test_addRegulation(self):
assert self.fixtures == ["test_setup", "login"]
Running these tests with pytest -vs (verbose mode, disabling stdout capture) produces:
...::TestLogin::test_login PASSEDTest is finished, fixtures used: ['test_setup', 'login']
...::TestAddRegulation::test_addRegulation PASSEDTest is finished, fixtures used: ['test_setup', 'login']
I have a following simplified code example:
from urllib.parse import urlparse, parse_qs
from selenium import webdriver
import pytest
#pytest.fixture(scope="module")
def driver():
options = webdriver.ChromeOptions()
_driver = webdriver.Chrome(options=options)
yield _driver
_driver.close()
#pytest.mark.parametrize("instance_name", ["instance1", "instance2"])
class TestInstance:
#pytest.fixture
def authorization_code(self, instance_name, driver):
driver.get(f"https://{instance_name}.com")
...do some UI actions here
authorization_code = parse_qs(urlparse(redirected_url).query)["code"][0]
#pytest.fixture
def access_token(self, authorization_code):
...obtain access_token here using authorization code
return "access_token"
def test_case_1(self, access_token):
...do some API calls using access_token
def test_case_2(self, access_token):
...do some API calls using access_token
What I would like to do is to execute UI actions in the authorization_code function once and obtain one access_token per instance.
Currently my UI actions are executed for every test case, leading to the fact that UI actions actually execute 2 * 2 = 4 times.
Is it possible to do with pytest?
Or maybe I am missing something in my setup?
In general I would just change the fixture scope: currently it gets recreated every time it is called, hence the reuse of ui actions. This is by design to ensure fixtures are clean. If your fixture didn't depend on the function-level fixture instance you could just put scope="class". (See the docs on scopes).
In this case I'd be tempted to handle the caching myself:
import pytest
from datetime import datetime
#pytest.mark.parametrize("instance", ("instance1", "instance2"))
class TestClass:
#pytest.fixture()
def authcode(self, instance, authcodes={}, which=[0]):
if not instance in authcodes:
authcodes[
instance
] = f"authcode {which[0]} for {instance} at {datetime.now()}"
which[0] += 1
return authcodes[instance]
def test1(self, authcode):
print(authcode)
def test2(self, authcode):
print(authcode)
(which is just used to prove that we don't regenerate the fixture).
This feels inelegant and I'm open to better ways of doing it.
I'm trying to create a test suite with pytest and Selenium using Page Object Models for pattern designing. For using my page classes on my tests, I just imported them inside my TestClass __init__ method, since they need to instanced with a driver.
I know that, by default, pytest ignores classes with an __init__ method. I also know, by reading here that it's possible to configure where pytest collects tests. Is it also possible to make it consider class tests with __init__, instead of returning an "Empty Suite" error?
#pytest.fixture(scope="session")
def driver_init(request):
from selenium import webdriver
driver = webdriver.Chrome()
session = request.node
page = PageFunctions(driver)
login_page = LoginPage(driver)
registration_page = RegistrationPage(driver)
for item in session.items:
cls = item.getparent(pytest.Class)
setattr(cls.obj, "driver", driver)
setattr(cls.obj, "page", page)
setattr(cls.obj, "login", login_page)
setattr(cls.obj, "registration", registration_page)
Pytest and Unittest have some different conventions. Mixing the two in the same test function is usually worth avoiding.
If you're working exclusively with Pytest, you would pass in your fixtures as arguments to your test function, e.g.:
import pytest
from selenium import webdriver
#pytest.fixture
def driver():
driver = webdriver.Chrome()
return driver
def test_func(driver):
# `driver` is found by pytest in the fixture above and
# automatically passed in
request = ... # Instantiate your request (not in your included code)
session = request.node
page = PageFunctions(driver)
login_page = LoginPage(driver)
registration_page = RegistrationPage(driver)
# Make some assertions about your data, e.g.:
assert page is not None
You haven't included all of the object definitions/imports so it's hard to see what you're trying to accomplish with the test, but hopefully that gives you an idea of the pytest conventions.
In test1.py I have below code
#pytest.fixture(scope="session")
def moduleSetup(request):
module_setup = Module_Setup()
request.addfinalizer(module_setup.teardown())
return module_setup
def test_1(moduleSetup):
print moduleSetup
print '...'
#assert 0
# def test_2(moduleSetup):
# print moduleSetup
# print '...'
# #assert 0
And in conftest.py I have
class Module_Setup:
def __init__(self):
self.driver = webdriver.Firefox()
def teardown(self):
self.driver.close()
When I run it launches and closes browser.
But I also get error self = <CallInfo when='teardown' exception: 'NoneType' object is not callable>, func = <function <lambda> at 0x104580488>, when = 'teardown'
Also If I want to run both tests test_1 and test_2 with same driver object I need to use scope module or session?
Regarding the exception
When using request.addfinalizer(), you shall pass in reference to a function.
Your code is passing result of calling that function.
request.addfinalizer(module_setup.teardown())
You shall call it this way:
request.addfinalizer(module_setup.teardown)
Regarding fixture scope
If your fixture allows reuse across multiple test calls, use "session"
scope. If it allows reuse only for tests in one module, use "module" scope.
Alternative fixture solution
The way you use the fixtures is not much in pytest style, it rather resembles unittest.
From the code you show it seems, the only think you need is to have running Firefox with driver allowing to use it in your tests, and after being done, you need to close it.
This can be accomplished by single fixture:
#pytest.fixture(scope="session")
def firefox(request):
driver = webdriver.Firefox()
def fin():
driver.close()
request.addfinalizer(fin)
or even better using #pytest.yield_fixture
#pytest.yield_fixture(scope="session")
def firefox(request):
driver = webdriver.Firefox()
yield driver
driver.close()
The yield is place, where fixture stops executing, yields the created value (driver) to test cases.
After the tests are over (or better, when the scope of our fixture is over), it
continues running the instructions following the yield and does the cleanup
work.
In all cases, you may then modify your test cases as follows:
def test_1(firefox):
print moduleSetup
print '...'
and the moduleSetup fixture becomes completely obsolete.
I'm trying to do setup and teardown modules using pytest-bdd.
I know with behave you can do a environment.py file with before_all and after_all modules. How do I do this in pytest-bdd
I have looked into "classic xunit-style setup" plugin and it didn't work when I tried it. (I know thats more related to py-test and not py-test bdd).
You could just declare a pytest.fixture with autouse=true and whatever scope you want. You can then use the request fixture to specify the teardown. E.g.:
#pytest.fixture(autouse=True, scope='module')
def setup(request):
# Setup code
def fin():
# Teardown code
request.addfinalizer(fin)
A simple-ish approach for me is to use a trivial fixture.
# This declaration can go in the project's confest.py:
#pytest.fixture
def context():
class Context(object):
pass
return Context()
#given('some given step')
def some_when_step(context):
context.uut = ...
#when('some when step')
def some_when_step(context):
context.result = context.uut...
Note: confest.py allows you to share fixtures between codes, and putting everything in one file gives me a static analysis warning anyway.
"pytest supports execution of fixture specific finalization code when the fixture goes out of scope. By using a yield statement instead of return, all the code after the yield statement serves as the teardown code:"
See: https://docs.pytest.org/en/latest/fixture.html
eg.
#pytest.fixture(scope="module")
def smtp_connection():
smtp_connection = smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
yield smtp_connection # provide the fixture value
print("teardown smtp")
smtp_connection.close()