Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow methods to access a class fixture. #3359

Closed
Drew-Ack opened this issue Mar 30, 2018 · 8 comments
Closed

Allow methods to access a class fixture. #3359

Drew-Ack opened this issue Mar 30, 2018 · 8 comments
Labels
type: question general question, might be closed after 2 weeks of inactivity

Comments

@Drew-Ack
Copy link

Drew-Ack commented Mar 30, 2018

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.

@pytestbot
Copy link
Contributor

GitMate.io thinks possibly related issues are #2938 (fixture with scope “class” running for every method), #2145 (Session scoped fixtures can leak out of a class), #2392 (Allow a fixture to be reset on a given condition), #1376 (allow fixtures to provide extra info when a test fails), and #2947 (Marking subclass affected methods of parent class.).

@brianmaissy
Copy link
Contributor

brianmaissy commented Apr 2, 2018

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

@nicoddemus
Copy link
Member

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?

@nicoddemus nicoddemus added the type: question general question, might be closed after 2 weeks of inactivity label Apr 2, 2018
@Drew-Ack
Copy link
Author

Drew-Ack commented Apr 3, 2018

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.

@nicoddemus
Copy link
Member

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? 😕

@RonnyPfannschmidt
Copy link
Member

this is potentially just driving acceptance tests at the whole system level (at least i hope so)

@Drew-Ack
Copy link
Author

Drew-Ack commented Apr 5, 2018

I should have been even more specific in my layout of the test project.

  1. This project is not coupled with the application it is testing. The application is seperate from this testing suite I created. My testing project interacts with the front-end of the application, simulating a user. I beleive the correct term is continual automated integration testing.
  2. The tools I use are pytest, selenium and python.

@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.

What it used to be.

The following code is what i think is relevant from the entire project. I can include more if neccessary.

Login.py

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(
            ... 

test_login.py

from tests.Login import Login
 
# This is a test function.
def test_login(browser)
    Login.login(browser)
    # testing stuff
    Login.logout(browser)

conftest.py

@pytest.fixture(scope="session")
browser(request)
#This fixture uses pytests request object to collect parsed command line options from the request.config object. The command line options are parsed from the relative pytest hook.
# This fixture yields a selenium webdriver object at session level for any test functions to be able to pick up.
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

@nicoddemus
Copy link
Member

nicoddemus commented Apr 5, 2018

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. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: question general question, might be closed after 2 weeks of inactivity
Projects
None yet
Development

No branches or pull requests

5 participants