Create multiple similar PyTest fixtures - python

I have a series of pytest fixtures which are very similar to each other in nature. The fixtures are passed to tests which verify that certain CSS selectors work properly. The code within each fixture is nearly the same; this only difference is that a different URL is passed to the page.goto() function for each fixture.
Each fixture looks something like this:
import pytest
#pytest.fixture(scope="module")
def goto_pagename():
with sync_playwright() as play:
browser = play.chromium.launch()
page = browser.new_page()
page.goto(TestScrapeWebsite.address)
yield page
browser.close()
I tried to use a decorator which covers all of the code except page.goto() and yield page, which is below:
from playwright.sync_api import sync_playwright
def get_page(func):
def wrapper(*args, **kwargs):
with sync_playwright() as play:
browser = play.chromium.launch(*args, **kwargs)
page = browser.new_page()
func(page)
browser.close()
return wrapper
Then, the fixtures and tests would look something like this:
import pytest
#pytest.fixture(scope="module")
#get_page
def google_page(page):
page.goto(TestScrapeGoogle.address)
yield page
#pytest.fixture(scope="module")
#get_page
def stackoverflow_page(page):
page.goto(TestScrapeStackOverflow.address)
yield page
#pytest.fixture(scope="module")
#get_page
def github_page(page):
page.goto(TestScrapeGitHub.address)
yield page
class TestScrapeGoogle:
address = "https://google.com/"
def test_selectors(self, google_page):
assert google_page.url == self.address
class TestScrapeStackOverflow:
address = "https://stackoverflow.com/"
def test_selectors(self, stackoverflow_page):
assert stackoverflow_page.url == self.address
class TestScrapeGitHub:
address = "https://github.com/"
def test_selectors(self, github_page):
assert github_page.url == self.address
However, when running the pytest test runner, exceptions were raised concerning the fixtures:
$ pytest test_script.py
...
============================================================================================ short test summary info =============================================================================================
FAILED test_script.py::TestScrapeGoogle::test_selectors - AttributeError: 'NoneType' object has no attribute 'url'
FAILED test_script.py::TestScrapeStackOverflow::test_selectors - AttributeError: 'NoneType' object has no attribute 'url'
FAILED test_script.py::TestScrapeGitHub::test_selectors - AttributeError: 'NoneType' object has no attribute 'url'
Is there a way to modify the approach which I have taken in order to simplify each of the fixtures? Or, is what I am asking out of the capabilities of pytest, and do I just have to fully write out each fixture?
Similar Questions:
Below I added a few Stack Overflow questions which appear when the title of my question is searched. I also included a reason why the answers to for the question would not be an ideal solution for my issue.
Multiple copies of a pytest fixture: All the copies of the pytest fixture are for the same tests. In my case, I'm expecting to have a separate fixture for each test.
Run a test with two different pytest fixtures: While there are multiple fixtures, each test will (likely) only have one fixture decorating the test.

Edit: I'm not able to run this code myself, as my chromeium setup seems busted at this time
I would opt for using pytest's parametrize functionality to do what you expect.
First, I would create my fixtures
#pytest.fixture(scope='module')
def browser():
with sync_playwright() as play:
browser = play.chromium.launch(*args, **kwargs)
try:
yield browser
finally:
# Ensure the browser is gracefully closed at end of test
browser.close()
#pytest.fixture
def page(browser, url):
# Provide the page
page = browser.new_page()
page.goto(url)
return page
Note that the url fixture is not yet defined.
Next, I create one test with the url (and expected url) parametrized
#pytest.mark.parametrize('url, expected_url', [
('https://google.com/', 'https://google.com/'),
('https://stackoverflow.com/', 'https://stackoverflow.com/'),
('https://github.com/', 'https://github.com/'),
])
def test_selectors(page, expected_url):
assert page.url == expected_url
Alternatively, if the tests for the web sites are slightly different, you can have three separate tests, each having a single parametrized entry.
#pytest.mark.parametrize('url, expected_url', [
('https://google.com/', 'https://google.com/'),
])
def test_google_selector(page, expected_url):
assert page.url == expected_url
#pytest.mark.parametrize('url, expected_url', [
('https://stackoverflow.com/', 'https://stackoverflow.com/'),
])
def test_stackoverflow_selector(page, expected_url):
assert page.url == expected_url

Related

Using a class-based test as a fixture

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']

How to pass test status to it's teardown, through a fixture preferably

I have a BaseTest class which has tear_down and I want to have inside tear_down a variable representing wether or not the test has failed.
I tried look at A LOT of older posts but I coulden't implement them as they were hooks or mixture of hook and fixture and something did not work on my end.
What is the best practice for doing that?
Last thing I've tried was -
#pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item):
outcome = yield
rep = outcome.get_result()
# set a report attribute for each phase of a call, which can
# be "setup", "call", "teardown"
setattr(item, "rep_" + rep.when, rep)
Then pass request fixture to teardown and inside use
has_failed = request.node.rep_call.failed
But request had no attributes at all, it was a method.
Also tried -
#pytest.fixture
def has_failed(request):
yield
return True if request.node.rep_call.failed else False
and pass it like that.
def teardown_method(self, has_failed):
And again, no attributes.
Isn't there a simple fixture to just do like request.test_status or something like that?
It's important that the teardown will have that bool parameter wether or not it failed and not do stuff outside the teardown.
Thanks!
There doesn't appear to be any super simple fixture offering the test report as a fixture. And I see what you mean: most examples of recording the test report are geared toward non-unittest use cases (including the official docs). However, we can adjust these examples to work with unittest TestCases.
There appears to be a private _testcase attribute on the item arg passed to pytest_runtest_makereport, which contains the instance of the TestCase. We can set an attribute on it, which can then be accessed within teardown_method.
# conftest.py
import pytest
#pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call' and hasattr(item, '_testcase'):
item._testcase.did_pass = report.passed
And here's a dinky little example TestCase
import unittest
class DescribeIt(unittest.TestCase):
def setup_method(self, method):
self.did_pass = None
def teardown_method(self, method):
print('\nself.did_pass =', self.did_pass)
def test_it_works(self):
assert True
def test_it_doesnt_work(self):
assert False
When we run it, we find it prints the proper test failure/success bool
$ py.test --no-header --no-summary -qs
============================= test session starts =============================
collected 2 items
tests/tests.py::DescribeIt::test_it_doesnt_work FAILED
self.did_pass = False
tests/tests.py::DescribeIt::test_it_works PASSED
self.did_pass = True
========================= 1 failed, 1 passed in 0.02s =========================

How to include test classes with init in a Pytest test suite?

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.

How can I more accurately report Pytest Selenium passes and failures to SauceLabs?

Trying to find the most elegant way to inform the test fixture of a test failure. This test fixture needs to report the results of the test to saucelabs in order to mark it as pass or fail. I've tried to delete as much irrelevant code from these examples as possible.
The following test uses the fixture browser.
def test_9(browser):
browser.get(MY_CONSTANT)
assert "My Page" in browser.title
browser.find_element_by_css_selector('div > img.THX_IP')
browser.find_element_by_link_text('Some text').click()
... etc
The fixture browser, which currently is hard coded to mark the test as passed:
#pytest.fixture()
def browser(request):
driver_type = request.config.getoption('driver')
if driver_type == 'sauce':
driver = webdriver.Remote(
command_executor = 'MY_CREDENTIALS',
desired_capabilities = caps)
else:
driver = webdriver.Chrome()
driver.implicitly_wait(2)
yield driver
if driver_type == 'sauce':
sauce_client.jobs.update_job(driver.session_id, passed = True)
driver.quit()
I've discovered a few workarounds but I'd really like to know the best way to do it.
I have the following fixture in my conftest.py file which handles all of the pass/fail reporting to sauce. I am not sure if it's "the best way to do it" but it certainly works for us at the moment. We include this fixture for every test that we write.
There's considerably more to this fixture but this section contained at the bottom handles the report really well.
def quit():
try:
if config.host == "saucelabs":
if request.node.result_call.failed:
driver_.execute_script("sauce:job-result=failed")
elif request.node.result_call.passed:
driver_.execute_script("sauce:job-result=passed")
finally:
driver_.quit()

pytest exception none type object is not callable

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.

Categories