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

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.

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

Pytest class-instance level fixture inside parametrized class

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.

Create multiple similar PyTest fixtures

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

Pytest fixture inside class to execute only once

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.

pytest -> How to use fixture return value in test method under a class

I have a fixture that returns a value like this:
import pytest
#pytest.yield_fixture(scope="module")
def oneTimeSetUp(browser):
print("Running one time setUp")
if browser == 'firefox':
driver = webdriver.Firefox()
print("Running tests on FF")
else:
driver = webdriver.Chrome()
print("Running tests on chrome")
yield driver
print("Running one time tearDown")
This fixture gets the browser value from another fixture which is reading the command line option.
Then I have a test class where I have more than one test methods and they all want to consume the same returned value driver to proceed the tests.
import pytest
#pytest.mark.usefixtures("oneTimeSetUp")
class TestClassDemo():
def test_methodA(self):
# I would like to use the driver value here
# How could I do this?
# Something like this
self.driver.get("https://www.google.com")
self.driver.find_element(By.ID, "some id")
print("Running method A")
def test_methodB(self):
print("Running method B")
Using self.driver fails with the error message
self = <test_class_demo.TestClassDemo object at 0x102fb6c18>
def test_methodA(self):
> self.driver.get("https://www.google.com")
E AttributeError: 'TestClassDemo' object has no attribute 'driver'
I am aware that I can pass the fixture as an argument to every method where I want to use that, but that is not the best way because I need this in every method and it should be possible to pass it to the class and then use it in all the test methods.
What is the best way that I can make the driver object available to the methods?
EDIT 1:
Created the fixture in conftest.py like this as suggested
#pytest.yield_fixture(scope="class") # <-- note class scope
def oneTimeSetUp(request, browser): # <-- note the additional `request` param
print("Running one time setUp")
if browser == 'firefox':
driver = webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(3)
print("Running tests on FF")
else:
driver = webdriver.Chrome()
print("Running tests on chrome")
## add `driver` attribute to the class under test -->
if request.cls is not None:
request.cls.driver = driver
## <--
yield driver
print("Running one time tearDown")
I have one more class, which object in need in the TestClassDemo and I need to pass the same driver instance to the class. Consider it as class ABC
class ABC():
def __init(self, driver):
self.driver = driver
def enterName(self):
# Do something with driver instance
Then in the TestClassDemo
#pytest.mark.usefixtures("oneTimeSetUp", "setUp")
class TestClassDemo(unittest.TestCase):
# I need to create an object of class ABC, so that I can use it here
# abc = ABC(self.driver)
#pytest.fixture(scope="class", autouse=True)
def setup(self):
self.abc = ABC(self.driver)
# I tried this, but it's not working
# This error message shows up
# AttributeError: 'TestClassDemo' object has no attribute 'driver'
def setup_module(self):
self.abc = ABC(self.driver)
# This also does not work
# Error message -> AttributeError: 'TestClassDemo' object has no attribute 'abc'
def test_methodA(self):
self.driver.get("https://google.com")
self.abc.enterName("test")
print("Running method A")
def test_methodB(self):
self.abc.enterName("test")
print("Running method B")
This abc object should be usable in other test_ methods also.
All these classes are in separate modules, I mean to say in separate .py files.
Also please explain in the answer what is the best way to use instead of yield driver instance.
EDIT 2:
For this example without yield, what would be the best way to run oneTimeTearDown also? I was running the tearDown steps after the yield
#pytest.fixture(scope="class")
def oneTimeSetUp(request, browser):
print("Running one time setUp")
if browser == 'firefox':
driver = webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(3)
print("Running tests on FF")
else:
driver = webdriver.Chrome()
print("Running tests on chrome")
if request.cls is not None:
request.cls.driver = driver
Also I tried using UnitTest class, but when I use def setUpClass(cls), I was not able to use the objects instantiated in the test_ methods. So I couldn't not figure out how to achieve that.
I also wanted to provide command line arguments like browser from the command line and when I tried unittest, I had to write the command line argument in every class. I wanted to provide them in one place only, like a test suite. So conftest helped me here.
I had a question on stackoverflow but didn't get a response. Could you please take a look at that also?
Python unittest passing arguments to parent test class
Thanks
Thanks
There's a technique outlined in the py.text unittest integration documentation that may be helpful to you ... using the built-in request fixture. Otherwise, I'm not aware of way to access the return value of a fixture without providing the named fixture as a method param.
#pytest.yield_fixture(scope="class") # <-- note class scope
def oneTimeSetUp(request, browser): # <-- note the additional `request` param
print("Running one time setUp")
if browser == 'firefox':
driver = webdriver.Firefox()
print("Running tests on FF")
else:
driver = webdriver.Chrome()
print("Running tests on chrome")
## add `driver` attribute to the class under test -->
if request.cls is not None:
request.cls.driver = driver
## <--
yield driver
print("Running one time tearDown")
Now you can access the driver as a class attribute in TestClassDemo, as you have in your example (i.e. self.driver should work).
The caveat is that your fixture must use scope='class', otherwise the request object will not possess a cls attribute.
I hope that helps!
UPDATE
I have one more class, which object in need in the TestClassDemo and I need to pass the same driver instance to the class. Consider it as class ABC
It's difficult to know without more context, but it seems to me that you can probably get away with instantiating an ABC object at the same time that you instantiate the driver ... in the oneTimeSetUp fixture. For example ...
#pytest.yield_fixture(scope="class")
def oneTimeSetUp(request, browser):
print("Running one time setUp")
if browser == 'firefox':
driver = webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(3)
print("Running tests on FF")
else:
driver = webdriver.Chrome()
print("Running tests on chrome")
if request.cls is not None:
request.cls.driver = driver
request.cls.abc = ABC(driver) # <-- here
yield driver
print("Running one time tearDown")
But if you only need the ABC instance for a test class or two, here's how you might use a fixture inside the class definition ...
#pytest.mark.usefixtures("oneTimeSetUp", "setUp")
class TestClassDemo(unittest.TestCase):
#pytest.fixture(autouse=True)
def build_abc(self, oneTimeSetUp): # <-- note the oneTimeSetup reference here
self.abc = ABC(self.driver)
def test_methodA(self):
self.driver.get("https://google.com")
self.abc.enterName("test")
print("Running method A")
def test_methodB(self):
self.abc.enterName("test")
print("Running method B")
I wouldn't be particularly happy with the second example. A third option would be to have another yield_fixture, or similar, that is completely separate from oneTimeSetUp and returns an ABC instance with the driver already wrapped.
Which way is best for you? Not sure. You'll need to decide based on what you're working with.
It's proper to note for posterity that pytest fixtures are just sugar and a bit of magic. You are not required to use them at all, if you find them difficult. pytest is happy to execute vanilla unittest TestCases.
Also please explain in the answer what is the best way to use instead of yield driver instance.
Here's what I had in mind ...
#pytest.fixture(scope="class")
def oneTimeSetUp(request, browser):
print("Running one time setUp")
if browser == 'firefox':
driver = webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(3)
print("Running tests on FF")
else:
driver = webdriver.Chrome()
print("Running tests on chrome")
if request.cls is not None:
request.cls.driver = driver
... notice that this doesn't return (or yield) the driver object, which means that it's no longer useful to provide this fixture as a named parameter to a function/method, which should be fine if all of your test cases are written as classes (suggested by your examples).
However, if you want to use the fixture as a named parameter, don't do this.

Categories