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

xxx() missing 1 required positional argument: 'mocker' #174

Closed
thediveo opened this issue Dec 7, 2019 · 17 comments
Closed

xxx() missing 1 required positional argument: 'mocker' #174

thediveo opened this issue Dec 7, 2019 · 17 comments

Comments

@thediveo
Copy link

thediveo commented Dec 7, 2019

I'm using pytest 5.3.0 with pytest-mock 1.12.1, and in a pytest class I want to temporarily mock a function of an object returned by some other module function. When I use test_calls_operation(mocker) outside a test class, then the mocker arg gets filled in by pytest-mock as expected. However, when I declare the test as shown below, I get test_calls_operation() missing 1 required positional argument: 'mocker'. What am I doing wrong? I sifted through the documentation and especially StackOverflow, but I seem to miss the knack of getting the mocker arg` automatically set as needed.

class KeysModuleTests(TestCase):
    def test_calls_operation(self, mocker):
        keys = mnt.keys.end()
        mocker.spy(keys, 'op_end')
        # ...
@twmr
Copy link

twmr commented Dec 7, 2019

Please try to inherit your test class from "object" instead. (see the code in https://docs.pytest.org/en/latest/fixture.html#autouse-fixtures-xunit-setup-on-steroids)

@thediveo
Copy link
Author

thediveo commented Dec 7, 2019

The problem with inheriting from object instead of TestCase is that VisualStudio Code cannot detect test cases anymore, so I'm loosing the test integration with pytest.

@nicoddemus
Copy link
Member

@thediveo

pytest fixtures cannot be used in unittest.TestCase test methods. The unittest integration in pytest uses the unittest Runner to actually run the tests, and the unittest runner doesn't know about fixtures (this applies to any fixture, not only mocker).

One workaround is to use an autouse fixture (which work un unittest.TestCase subclasses) to obtain the fixture you are interested in, then use it later. Example:

class KeysModuleTests(TestCase):

    @pytest.fixture(autouse=True)
    def __inject_fixtures(self, mocker):
        self.mocker = mocker

    def test_calls_operation(self):
        keys = mnt.keys.end()
        self.mocker.spy(keys, 'op_end')
        # ...

Closing for now but feel free to follow up with further questions.

@thediveo
Copy link
Author

thediveo commented Dec 7, 2019

Thank you for the explanation and workaround, that helps me get going. I've create an issue with the VSC python plugin to support discovery of non-Testcase derived test classes.

@thediveo
Copy link
Author

thediveo commented Dec 7, 2019

Just in case others find this issue, I've wrapped your workaround slightly differently in a simple fixture, so I can either use it on a class or a single method using @pytest.mark.usefixtures('mocka'):

@pytest.fixture(scope='function')
def mocka(request, mocker):
    """Exposes pytest-mock's "mocker" as "self.mocker" in
    unittest.TestCase-based tests.
    """
    request.instance.mocker = mocker

Thank you again for setting me on the right track!

@nicoddemus
Copy link
Member

Glad you could get going.

Btw, didn't you forget to pass autouse=True to your mocka example above?

@thediveo
Copy link
Author

thediveo commented Dec 8, 2019

Actually, no. Because I have a larger set of tests and only enable three fixture on some sets by explicitly specifying it.

@nicoddemus
Copy link
Member

I thought your test suite was unittest based, in which case it is not simple to just enable the fixture, that's why I asked.

@nicoddemus
Copy link
Member

Hi @alterEgo123,

That's because each test method in your class is executed with a new TestPlace instance (that's how pytest works and not specific to mock). One workaround is to set the shared attribute in the class instead of in the instance:

class TestPlace(AsyncHTTPTestCase):
    @pytest.fixture(autouse=True, scope='function')
    def mocka(self):
        TestPlace.shared = 0

    def test_login(self):
        response = self.fetch('/places/login', method='POST',
                              headers=None,
                              body=json_encode({
                                  'email': email,
                                  'password': 'verystrongpassword'
                              }),
                              )
        TestPlace.shared = ast.literal_eval(response.body.decode('ascii'))['token']

Full disclosure, tests should not independent from one another, meaning that a test should not depend on another test being run first. Otherwise you will be creating coupling between your tests, which is not healthy/recommended.

@alterEgo123
Copy link

Thanks a lot !
I can only test some functionalities if I log in and retrieve a token. How do propose I do that ?

@nicoddemus
Copy link
Member

It really depends on the case, but if you have two behaviors that are tightly coupled (login and then check a token) it is probably better to do both checks into the same test method.

@alterEgo123
Copy link

I need to test functionalities like resetting a password, listing Users as an admin, managing roles, etc. All of these rely on a JWT token in the headers. Should I rewrite the login and token extracting code in all of these tests ?

@nicoddemus
Copy link
Member

nicoddemus commented Aug 28, 2020

Should I rewrite the login and token extracting code in all of these tests ?

Ahh I see. No, what is usually done is to move that logic to its own fixture, then you can reuse it as appropriate:

class TestPlace(AsyncHTTPTestCase):

    @pytest.fixture
    def admin(self):
        response = self.fetch(...
                              body=json_encode({
                                  'email': "admin@example.com",
                                  'password': 'verystrongpassword'
                              }),
                              )
        token = ast.literal_eval(response.body.decode('ascii'))['token']
        ...
        return User(token)

    def test_admin_access(self, admin_user):
        assert can_delete_project(admin_user)

Something like that.

Fixtures are a great way to share common "setup" code between tests.

@alterEgo123
Copy link

It returns missing 1 required positional argument 😔

@joshuaspear
Copy link

joshuaspear commented Apr 14, 2023

Hi, I am receiving "AttributeError: 'GbtEst_Test' object has no attribute 'mocker'" when trying to use the above example, I wondered if anyone could help please! Thanks in advance

Class attempting to test

from sklearn.ensemble import GradientBoostingClassifier
from typing import Callable
import numpy as np

class BehavEst:
    
    def __init__(self, estimator:Callable) -> None:
        self.estimator = estimator
    
    def eval_pdf(self, indep_vals, dep_vals):
        raise NotImplementedError

class GbtEst(BehavEst):
    
    def __init__(self, estimator:GradientBoostingClassifier) -> None:
        super().__init__(estimator=estimator)
    
    def eval_pdf(self, indep_vals:np.array, dep_vals:np.array):
        slct_msk = (np.array(range(0,len(indep_vals))), dep_vals)
        probs = self.estimator.predict_proba(X=indep_vals)
        return probs[slct_msk]

Test file

import unittest
import pytest
import numpy as np

@pytest.fixture(scope='function', autouse=True)
def mocka(obj, mocker):
    """Exposes pytest-mock's "mocker" as "self.mocker" in
    unittest.TestCase-based tests.
    """
    obj.instance.mocker = mocker

behav_est =  GbtEst(estimator=GradientBoostingClassifier())   
class GbtEst_Test(unittest.TestCase):
    
    @pytest.mark.usefixtures('mocka')
    def test_1_eval_pdf(self):
        # Dependant values range between 1 and 5
        num_features = 10
        # correct_pred = [
        #     True, False, False, True, False, True, True
        #     ]
        dep_vals_input = np.array([1,2,1,5,2,4,1])
        indep_vals_input = np.arange(0,len(dep_vals_input)*num_features)
        indep_vals_input = indep_vals_input.reshape(-1,num_features)
        np.random.seed(seed=1)
        mocked_value = np.random.uniform(
            0,1,len(dep_vals_input)*dep_vals_input.max()).reshape(
                len(dep_vals_input), dep_vals_input.max())
        actual_res = []
        for idx, vals in zip(dep_vals_input, mocked_value):
            actual_res.append(vals[idx-1])
        actual_res = np.array(actual_res)
        dep_vals_input=dep_vals_input.reshape(-1,1)
        estimator_mock = self.mocker.patch("behav_est.estimator.predict_proba")
        estimator_mock.return_value = mocked_value
        
        pred_res = behav_est.eval_pdf(
            indep_vals=indep_vals_input, dep_vals=dep_vals_input)
        self.assertTrue(pred_res==actual_res)

@nicoddemus
Copy link
Member

nicoddemus commented Apr 15, 2023

Hi,

You are accessing self.mocker.patch, but self does not have that attribute.

You want to declare the mocker fixture as a parameter, and use it as such:

class GbtEst_Test(unittest.TestCase):
    
    @pytest.mark.usefixtures('mocka')
    def test_1_eval_pdf(self, mocker):
        ...
        estimator_mock = mocker.patch("behav_est.estimator.predict_proba")

Declaring the parameter will cause pytest to inject the mocker fixture for you.

PS: In the future always try to post the full traceback along the example code when reporting bugs in projects. 👍
PS2: Avoid "hijacking" closed issues, prefer to open new ones; if you think they are related, you can add a note such as "I think this is related to #XXX" to the new issue.

Hope that helps!

@joshuaspear
Copy link

Thank you @nicoddemus - I tried implementing your fix however, I seem to be receiving the "positional argument error" that OP mentioned.
I appreciate your feedback re issue posting - thank you! I've opened a new issue in #352

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants