I have a class I use for logging in :
class Login:
@staticmethod
def login(browser,login_url):
#Login stuff
browser is a fixture.
@pytest.fixture(scope="session")
def browser(request):
# yield a browser for the test session
login_url is a NEW fixture.
@pytest.fixture(scope="session")
def login_url(environment): #enviroment is another fixture that yields a command line option value.
# yield a url for logging in.
When ever I called login in other scripts, it had the signature of
Login.login(browser)
Well, adding the fixture login_url as a new additional parameter to login, has changed the signature to Login.login(browser,login_url). This breaks every single call to everytime i call the login method because the function signature has changed.
My problem is there has to be a better way to have functions within the Login class to have access to the url that login_url provides without having to be so explicit in the method signatures within the Login class.
I suppose I want a fixture that operates at the class level and exposes a value that the functions can pick up and go on their marry way.
So after scouring the webs for solutions I come to the developers and maintainers for assistance.
Ive looked into; autouse, parameterization, and marking at class level. I cant get any of them to do what I need, but I also might be using them wrong.
GitMate.io thinks possibly related issues are https://github.com/pytest-dev/pytest/issues/2938 (fixture with scope βclassβ running for every method), https://github.com/pytest-dev/pytest/issues/2145 (Session scoped fixtures can leak out of a class), https://github.com/pytest-dev/pytest/issues/2392 (Allow a fixture to be reset on a given condition), https://github.com/pytest-dev/pytest/issues/1376 (allow fixtures to provide extra info when a test fails), and https://github.com/pytest-dev/pytest/issues/2947 (Marking subclass affected methods of parent class.).
if Login is really a business logic class, you wouldn't really want it to be dependent on pytest, would you? I would evaluate the login_url fixture a single time, pass it to the constructor (or a method) of the Login class, and then return that instance from a session-scoped fixture
Agree with @brianmaissy if I'm understanding the question correctly.
class Login:
@staticmethod
def login(browser,login_url):
#Login stuff
When ever I called login in other scripts, it had the signature of
Login.login(browser)
How is that possible if Login.login clearly requires two arguments, browser and login_url?
I should have been more specific,
Old Login method.
@staticmethod
def login(browser):
#Login stuff
New Login Method
@staticmethod
def login(browser,login_url):
#Login stuff
Because I have my login method requesting fixtures, its signature changes as I add more fixture dependencies to it. I use fixtures because theyve been convenient when setting up tests. Need a reference to the browser? I used a fixture. Need to know what environment your in (alpha, gamma, etc.)? Use a fixture.
@brianmaissy If i understand correctly, I would have:
@pytest.fixture(scope="session")
def login(environment): #enviroment is another fixture that yields a command line option value.
... # determine what the url is, www.alpha.com, www.gamma.com, etc.
Login(new_url); #Use a constructor to build a Login object
yield Login #yield that object when its needed.
I think my problem im encountering is that Im depending on fixtures and trying to leverage pytest way more than I should be.
Hi @Drew-Ack,
I will assume you meant your code to be this:
class Login:
@staticmethod
def login(browser):
# login stuff
because of the @staticmethod decorator.
Because I have my login method requesting fixtures, its signature changes as I add more fixture dependencies to it.
Not sure I follow, in pytest only test functions can "request" fixtures (actually their parameters are considered fixtures which pytest injects when it calls the test functions).
I use fixtures because theyve been convenient when setting up tests. Need a reference to the browser? I used a fixture. Need to know what environment your in (alpha, gamma, etc.)? Use a fixture.
It is not correct to make your test framework drive your application, that's not what a test framework is meant to do. How do you run your application, by executing pytest test_app.py instead of python app.py? π
this is potentially just driving acceptance tests at the whole system level (at least i hope so)
I should have been even more specific in my layout of the test project.
@nicoddemus You are correct in saying that my login function in my Login class does not request a fixture. I do however have the test functions request fixtures and pass them into the login functions as arguments, That is why their signature was growing anytime I needed to extend their functionality.
The following code is what i think is relevant from the entire project. I can include more if neccessary.
class Login(object):
DEFAULT_COMPANY = "company"
DEFAULT_USERNAME = "username"
DEFAULT_PASSWORD = "password"
DEFAULT_EMAIL = "email"
DEFAULT_URL = "url"
@staticmethod
def login(browserl): #i pass in a browser object here, i request browser as a fixture at a test function level and pass it in here so this function login has access to the browser.
browser.get(DEFAULT_URL)
WebDriverWait(self.browser, 5, .5, NoSuchElementException).until(
...
from tests.Login import Login
# This is a test function.
def test_login(browser)
Login.login(browser)
# testing stuff
Login.logout(browser)
```
@pytest.fixture(scope="session")
browser(request)
yield browser
browser.close( ) # Browser closes it self at end.
My largest issue came when we started adding options to switch environments; alpha, gamma, etc.
My login function in my login class had a static string it used for the url. This string has a substring in it that defines what environment I am in. www.alpha.app.com, www.gamma.app.com.
I realized that the static string it used on the url should now be more dynamic.
I created a fixture and command line options that allow a user of my project to select which environment, --environment alpha.
I created a new fixture that requests the environment fixture and builds the string that is used for the login function. This fixture was called:
@pytest.fixture(scope="session")
def login_url(environment):
#build the url
yield # the built url
And it worked, but to allow my login function from my Login class to be able to access this session level object, i now had to increase the login function signature to include login_url.
@staticmethod
def login(browser, login_url): # this is getting larger.
browser.get(login_url)
WebDriverWait(self.browser, 5, .5, NoSuchElementException).until(
...
# What it now is
So after looking at that and thinking to myself, nooooo, there has to be a better way, I decided to look at what hole ive gone down and see how to make this better. The following is the current way.
## conftest.py
I now have a fixture at session level that requests the browser and login_url fixtures. It instantiates a Login object that now test functions can request and use.
@pytest.fixture(scope="session")
def login(browser, login_url):
"""Yields a Login object. This object is created once and only once. This function will give out references to whomever calls it. """
login_object = Login(browser,login_url)
yield login_object
## Login.py
class Login(object):
DEFAULT_COMPANY = comapny
DEFAULT_USERNAME = username
DEFAULT_PASSWORD = password
DEFAULT_EMAIL = email
def __init__(self, browser, login_url):
self.browser = browser
self.url = login_url
def login(self):
self.browser.get(url)
## test_login.py
def test_login_green(browser,login): #reach out to the login fixture to get that Login object.
login.login()
...
```
I now have a "cleaner" way of expanding my login object via fixtures. Now i dont use fixtures for everything, but it does seem the easiest way to go when im passing around common things my tests need to run. My Login class does depend on fixtures, and im not sure if thats good or bad.
After explaining this though, it does seem kind of messed up. Am i off the wagon here? @RonnyPfannschmidt am i doing this completely wrong? Yay junior level development. :D
@nicoddemus To currently run my project I navigate to the root folder of my project and I type:
pytest [command line options] and away my suite goes.
Is it wrong to do this? I mean, Ive been using pytest to how I tthought it should be used
Thanks for expanding the explanation @Drew-Ack, being verbose and explaining things thoroughly goes a long way to help others understand your problem without wasting time trying to guess what's going on. π
So if I understand your Login class is part of your test suite, which acts as a driver of the front end of the application that you are actually testing; this is fine and a good approach to the problem IMHO.
Being part of the test suite, is perfectly fine that your Login class depends on fixtures.
One of the benefits of fixtures is that they abstract away what your resources require to be created, which is exactly the problem you encountered yourself: previously all your tests created a Login instance manually:
def test_foo(browser):
Login.login(browser)
And so on for dozens of tests. This doesn't abstract away the signature of the login function (IOW what it requires to do its job), so when you needed to add another parameter you had to manually fix a lot of tests by hand.
You figured out the proper solution, you should change your login process into a fixture, that way tests don't need to care about what Login needs to do its job:
def test_foo(login):
login.login()
In summary, one (among many) benefits of fixtures is that they abstract away what the fixture resource needs to function, so if that changes in the future your tests remain unaffected, only the fixture code needs to be touched. That's invaluable and a point that many people seem to miss. Cool that you figured out the solution yourself. π
I'm closing this now, but feel free to follow with the discussion. π
Most helpful comment
if
Loginis really a business logic class, you wouldn't really want it to be dependent on pytest, would you? I would evaluate thelogin_urlfixture a single time, pass it to the constructor (or a method) of theLoginclass, and then return that instance from a session-scoped fixture