How to call method on every py.test assertion failure? - python

Background:
I'm using py.test together with pytest-selenium, now I would like to take a screenshot of page when assertion fails.
Currently I have defined small helper method in my base page object class:
class PageBase(object):
def __init__(self,driver):
self.driver = driver
self.fake = Factory.create()
def screenshot(self,name):
self.driver.save_screenshot(datetime.now().strftime('%Y-%m-%d %H:%M:%S') + 'scr_'+name+'.png')
#contextmanager
def wait_for_page_load(self, timeout=45):
old_page = self.driver.find_element_by_tag_name('html')
yield
WebDriverWait(self.driver, timeout).until(
EC.staleness_of(old_page)
)
The problem is that I would like to make it automated mechanism instead of "manual" usage:
(test class example):
class TestLogin:
#allure.feature('Ability to login into admin panel')
def test_admin_login(self, prepare, page):
print URLMap.admin('test')
driver = prepare
driver.get(URLMap.admin(page))
login_page = LoginPage(driver)
assert login_page.is_page_correct(),'Login page not loaded correctly'
login_page.fill_login_data('testadmin','testadmin')
login_page.click_login_button()
assert login_page.is_user_logged_in(),'User cannot log in with provided credentials'
login_page.screenshot(page+'_logged_in')
How to run certain method for every assertion failure?

I personally haven't used but this might be your solution:
https://pytest.org/latest/example/simple.html#writing-well-integrated-assertion-helpers
Also this could help:
https://pytest.org/latest/assert.html#advanced-assertion-introspection

You have to use hooks.
https://docs.pytest.org/en/latest/example/simple.html#post-process-test-reports-failures
#pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
setattr(item, "rep_" + rep.when, rep)
return rep
#pytest.fixture(autouse=True, scope='session')
def driver(platform, request):
""" some driver setup code """
yield driver
if request.node.rep_call.failed:
try:
driver.get_screenshot_as_png()
except:
pass
driver.quit()
And if you want to attach a screenshot to allure report, simply do:
#pytest.fixture(autouse=True, scope='session')
def driver(platform, request):
""" some driver setup code """
yield driver
if request.node.rep_call.failed:
# Make the screen-shot if test failed:
try:
allure.attach(
driver.get_screenshot_as_png(),
name=request.function.__name__,
attachment_type=allure.attachment_type.PNG)
except:
""" do something """
driver.quit()

I think that screenShotInSelenium page should give you enough information regarding how you create a screenshot when an assert condition is met.
What you are missing is the use of #AfterMethod

Related

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

Quit driver instance in pytest after all the tests are executed

Below is the pytest class used to run 2 tests. Want to quit driver after both the tests are executed. used Teardown but it quits the driver after each test execution is completed
class FlightTest(unittest.TestCase):
driver = webdriver.Chrome(direct_path+'/resources/chromedriver.exe')
startup = StartUpPage(driver)
register = RegisterPage(driver)
def test_flight_registration(self):
dat = self.json_reader.read_from_file("testdata.json")
self.startup.navigate_to_url(dat['url'])\
.click_on_register_button()
self.register.create_user(dat['uid'], dat['pwd'], dat['con_pwd'])
def test_flight_sign_in(self,):
dat = self.json_reader.read_from_file("testdata.json")
self.startup.click_sign_in_link()
def tearDown(self):
self.driver.quit()
In unittest terms, you would need to use the setUpClass and tearDownClass class methods:
class FlightTest(unittest.TestCase):
#classmethod
def setUpClass(cls)
cls.driver = webdriver.Chrome()
cls.startup = StartUpPage(driver)
cls.register = RegisterPage(driver)
#classmethod
def tearDownClass(cls):
cls.driver.quit()
...
In pytest terms, you would create a class-scoped fixture:
import pytest
#pytest.fixture(scope="class")
def driver(request):
# code before 'yield' is executed before the tests
request.cls.driver = webdriver.Chrome()
request.cls.startup = StartUpPage(request.cls.driver)
request.cls.register = RegisterPage(request.cls.driver)
yield
# code after 'yield' is executed after the tests
request.cls.driver.quit()
#pytest.mark.usefixtures('driver')
class FlightTest(unittest.TestCase):
def test_spam(self):
self.driver.get('https://www.google.de')
def test_eggs(self):
self.driver.get('https://www.facebook.com')
An even better solution would be using the context manager property of the webdriver so it is automatically closed no matter what:
import pytest
#pytest.fixture(scope="class")
def driver(request):
with webdriver.Chrome() as driver:
request.cls.driver = driver
request.cls.startup = StartUpPage(driver)
request.cls.register = RegisterPage(driver)
yield
#pytest.mark.usefixtures('driver')
class FlightTest(unittest.TestCase):
def test_spam(self):
self.driver.get('https://www.google.de')
def test_eggs(self):
self.driver.get('https://www.facebook.com')

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.

How to return data from selenium to calling function in python

I am running a a selenium functional test using nose from inside a django function using:
arg = sys.argv[:1]
arg.append('--verbosity=2')
arg.append('-v')
out = nose.run(module=ft1.testy1, argv=arg, exit=False)
I have created the functional test using the selenium IDE. Part of the test looks like:
class y1(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.base_url = "https://www.yahoo.com/"
self.verificationErrors = []
self.accept_next_alert = True
def test_y1(self):
driver = self.driver
driver.get(self.base_url)
driver.find_element_by_link_text("Weather").click()
driver.save_screenshot('out1.png')
return " this is a returned value"
I want to return a string value (" this is a returned value") to the calling function. How can I do this?
The output of your nose.run() does not correspond to your test method. The default behavior for unittest.TestCase is to throw an exception. If you would like to signal an external code some specific detail, you can always do it through global/class variables, files, etc.
For example, this is how to do it with a class variable (results)
ft1_runner.py:
import nose
import ft1_test
if __name__ == '__main__':
out = nose.run(module=ft1_test, exit=False)
print 'Y1.test_y1 test results returned:', ft1_test.Y1.results['test_y1']
ft1_test.py:
import unittest
class Y1(unittest.TestCase):
results = {}
def test_y1(self):
Y1.results['test_y1'] = "this is a returned value"
I think it would help if you can describe the problem you are trying to solve: this seems a little awkward and error prone (what if the test is called twice, or test skipped, etc.)

Selenium tests fail after first test

I am trying to run selenium tests I've written for a Django project on a Debian server, using xvfb.
I have 3 tests I am trying to run, after the first test, they fail with this error:
NoSuchElementException: Message: u'Unable to locate element: {"method":"xpath","selector":"//a[#href=\\"#detail\\"]"}'
I have run export DISPLAY=:99 and am using Django LiveServerTestCase with django-selenium.
SELENIUM_DISPLAY = ':99' is set in my settings.py.
Here is my test runner:
class BaseLiveTest(LiveServerTestCase):
#classmethod
def setUpClass(cls):
cls.selenium = WebDriver()
super(BaseLiveTest, cls).setUpClass()
#classmethod
def tearDownClass(cls):
super(BaseLiveTest, cls).tearDownClass()
cls.selenium.quit()
def login(self, user):
#helper function, to log in users
#go to login page
self.selenium.get("%s%s" % (self.live_server_url, reverse('userena_signin')))
#wait for page to display
WebDriverWait(self.selenium, 10).until(
lambda x: self.selenium.find_element_by_id('id_identification'),
)
#fill in form and submit
identifictation_input = self.selenium.find_element_by_id('id_identification')
identifictation_input.send_keys(user.email)
password_input = self.selenium.find_element_by_id("id_password")
password_input.send_keys('password')
self.selenium.find_element_by_xpath('//form/descendant::button[#type="submit"]').click()
#wait for dashboard to load
WebDriverWait(self.selenium, 10).until(
lambda x: self.selenium.find_element_by_id('container'),
)
When I run each test by itself they all pass, but if I try to run them one after another the last 2 fail. Any ideas?
You need to use setUp() and tearDown(), not setUpClass() and tearDownClass(). The Class versions are run globally for the entire fixture, so all 3 tests are using the same WebDriver instance, and thus the browser isn't in the state you expect for your second and third tests.

Categories