Programing is a new thing for me and probably I am missing something obvious.
I would like to create a separate file and class for setup my webdriver for Appium tests, but I got errors like:
in test_login main_page = MainPage(self.driver)
AttributeError: 'test_Login_iOS' object has no attribute 'driver'
Currently I have two files: one with test case and other with methods for test steps:
test_Login_iOS.py:
class test_Login_iOS(unittest.TestCase):
def setUp(self):
logging.info("WebDriver request initiated. Waiting for response, this may take a while.")
# choose desired capabilities from desired_capabilities.py
desired_capabilities = DesiredCapabilities.desired_capabilities_for_iOS_iPad
self.driver = webdriver.Remote("http://localhost:4723/wd/hub", desired_capabilities)
self.driver.implicitly_wait(15) # seconds
def test_login(self):
logging.info("starting Test Case 1: login into active account")
welcome_page = WelcomePage(self.driver)
welcome_page.click_login_button()
login_page = LoginPage(self.driver)
and second file, page_ios.py:
class BasePage(unittest.TestCase):
"""
:type driver: appium.webdriver.Remote
"""
def __init__(self, driver):
super().__init__()
self.driver = driver
When I add new test case I have to add to it the same setUp method like in previous test case, so I would like to create a new class "Setup" that could be shared across multiple test cases.
Goal is to move setUp method to separate file and new class.
Related
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.
I currently have Python 3.7.3 with robotframework 3.1.1 and created a Python library (MySite.py) like this:
from selenium import webdriver
from collections import namedtuple
from HomePage import *
class Pages(object):
def __init__(self, handle):
self._pages = {}
self._home_window = handle
pass
#property
def home_window(self):
return self._home_window
#home_window.setter
def home_window(self, v):
self._home_window = v
#property
def homePage(self):
return self._pages['home']
def add(self, name, page):
self._pages[name] = page
def get(self, name):
return self._pages[name]
def getPages(self):
return self._pages
class MySite(object):
def __init__(self):
self._driver = None
#property
def driver(self):
return self._driver
#driver.setter
def driver(self, v):
self._driver = v
def close_all_windows(self):
# Close all windows
pages = self.pages.getPages()
for name, page in pages.items():
page.close()
def open_my_page(self):
self.driver = webdriver.Ie("IEDriverServer_Win32_3.141.0\\IEDriverServer.exe")
# Define pages
self.pages = Pages(self.driver.current_window_handle)
self.pages.add('home', HomePage(self.driver, self.pages))
# Open browser
self.driver.get(www.mypage.com)
# Code to wait to finish loading the page
When I create a Robot script to open and close the browser in one test case, it works:
*** Settings ***
Library MySite.py
*** Test Case ***
Open Browser to mypage.com and close browser
Open My Page
Close All Windows
But when I create a Robot script to open a browser in one test case and then on another test case to close it I get AttributeError: 'MySite' object has no attribute 'pages':
*** Settings ***
Library MySite.py
*** Test Case ***
Open Browser to mypage.com
Open My Page
Close Browser
Close All Windows
It seems like my instance variables are not being saved off for the second robot test. Do you know why this may be? Or what I am doing wrong?
By default, a new instance of the library is created for each test. You need to set the scope such that an instance of the class is created once for each suite or once for each test run.
This is mentioned in the user guide, in a section titled Test Library Scope:
Test libraries can control when new libraries are created with a class attribute ROBOT_LIBRARY_SCOPE. This attribute must be a string and it can have the following three values:
TEST CASE
A new instance is created for every test case. A possible suite setup and suite teardown share yet another instance. This is the default.
TEST SUITE
A new instance is created for every test suite. The lowest-level test suites, created from test case files and containing test cases, have instances of their own, and higher-level suites all get their own instances for their possible setups and teardowns.
GLOBAL
Only one instance is created during the whole test execution and it is shared by all test cases and test suites. Libraries created from modules are always global.
(emphasis mine)
To set the scope to "TEST SUITE" so that the instance is created only once for the whole suite, you would start your class definition like this:
class MySite(object):
ROBOT_LIBRARY_SCOPE = 'TEST SUITE'
How I can define custom function and then use it in test function, it works when I run single test case but don't work when I run multiple test cases.
class AlphaTest(unittest.TestCase):
def setUp(self):
self.driver = webdriver.Firefox()
self.driver.implicitly_wait(30)
self.driver.get("http://google.com/")
def asserTrueId(self, value):
self.assertTrue(self.driver.find_element_by_id(value))
time.sleep(1)
def test_flush_cache(self):
self.asserTrueId("block-menu-menu-menu-for-directories")
You could use unittest.setUpClass() to instantiate a class level driver instance. Similarly you can use tearDownClass() to clean up class level variables if required.
Since setUpClass() and tearDownClass() will only be run once inside your Test class you can use this to guarantee there is only one driver. Otherwise using setUp() would be run for each test method - each creating a browser instance each time - which could be slow and possibly memory inefficient.
class AlphaTest(unittest.TestCase):
#classmethod
def setUpClass(cls):
cls.driver = webdriver.Firefox()
cls.driver.implicitly_wait(30)
cls.driver.get("http://google.com/")
def asserTrueId(self, value):
self.assertTrue(self.driver.find_element_by_id(value))
time.sleep(1)
def test_flush_cache(self):
self.asserTrueId("block-menu-menu-menu-for-directories")
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.
I currently am doing some automated testing via Appium and python for testing an Android application. I'd like to abstract away some of the details so the tests read easier.
Right now I have just have an entire class doing the testing. For example, I'd like to turn this:
# authentication
self.driver = webdriver.Remote('http://localhost:4723/wd/hub', desired_caps)
username = self.driver.find_element_by_name('username')
password = self.driver.find_element_by_name('pw')
username.send_keys('some username')
password.send_keys('some password')
login_button = self.driver.find_element_by_name('Login')
login_button.click()
Into something like this:
Authentication.login(self.driver, 'largedata_lgabriel#hmbnet.com', 'R3DruM24')
Where our Authentication class might look like this:
class Authentication:
def login(driver, username, password):
input_username = driver.find_element_by_name('username')
input_password = driver.find_element_by_name('pw')
input_username.send_keys(username)
input_password.send_keys(password)
login_button = driver.find_element_by_name('Login')
login_button.click()
This would require creating an 'Authentication' class, but I'm unsure how to import these methods into my main testing class, and how to share the web driver object.
Here's how I structure this kind of thing. I also use unittest.TestCase which I want to highly recommend for any python automation; it'll allow you to lean on setUpClass, tearDownClass (one-time for all the derivative tests), and setUp, tearDown (before each test function) to do a lot of the set up and tear down stuff you need to do.
For my iOS-specific Appium tests:
Base file: ios_base.py
class iOSBase(unittest.TestCase):
#classmethod
def setUpClass(self):
# Set up logging if you want it
# Set up the XCode and iOS Simulator details you need if you're doing iOS
# Set up Appium
def setUp(self):
# Per test, I like to log the start of each test
self.log.info("--- {0} ---".format(self._testMethodName))
# etc. on setUp / tearDown stuff, whatever you need
# Helpful function like you have in mind
def login(self, username, password):
# You'll get self.driver for free by using setUpClass, yea!
Test file(s): test_login.py
from ios_base import iOSBase
class iOSLoginTests(iOSBase):
# Add any additional login-specific setUp you might want here, see also super()
def test_valid_login(self):
self.login(good_username, good_password)
def test_login_invalid_username(self):
self.login(bad_username, good_password)
# etc.