I'm using tweepy for an application using Twitter. In order to check if my code do what it is expected to do, I want to test it. But here the thing, tweepy module,at the end, consists of API requests. For example, I want to test this following is_already_liked function:
class twitter:
def __init__(self):
self.auth = tweepy.OAuth1UserHandler(
API_KEY, API_KEY_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET
)
self.api = tweepy.API(self.auth)
def is_already_liked(self, tweet_id: int) -> bool:
"""Checks if the tweet is already liked
Args:
tweet_id (int): id of the tweet
Returns:
bool: True if the tweet is already liked else False
"""
return self.api.get_status(tweet_id).favorited
I would like to test it on a fake tweet_id that I would create. Is there a way to do something like that with pytest and some mocking ?
Here is one way to do it:
In test_script.py
# module.submodule.script: where you define or import twitter
from module.submodule.script import twitter
class Tweet:
def __init__(self, favorited):
self.favorited = favorited
def test_is_already_liked(mocker):
api = mocker.patch("drafts.temp.tweepy.API")
api.return_value.get_status.side_effect = (
lambda x: Tweet(True) if x == "123456" else Tweet(False)
)
t = twitter()
assert t.is_already_liked("123456")
assert not t.is_already_liked("654321")
pytest .\tests\test_script.py
# Output: 1 passed
Related
I am new to pytest.
I am trying to mock/replace my client.py with fake_client.py for the testing.
The fake_client class contains the same methods as the client class.
Here is my project structure and code:-
abc/base_class.py
from .client import Client
class PushBase:
def __init__(self, hostname):
self.client = Client(hostname)
def process_product(self, item): # item type is dict {}
product_id = item.get('product_id')
if item.get('state') == 'present':
if not product_id:
# creating product
product_id = self.client.create_product(item.get('data'))
return product_id
# update product
self.client.update_product(item.get('data'))
elif item.get('state') == 'absent':
# delete product
self.client.delete_product(product_id)
This is my client.py with API calls
in abc/client.py
class Client:
def __init__(self, hostname):
self.hostname = hostname
# some other stuff
def create_product(self, params=None):
# some code
def update_product(self, params=None):
# some code
def delete_product(self, params=None):
# some code
I have created a fake client to test against the actual client.py
and it has the same methods as the client.py
in tests/fake_client.py
class FakeClient:
def __init__(self, *args, **kwargs):
pass
def create_product(self):
# some code
def update_product(self):
# some code
def delete_product(self):
# some code
in tests/test_base_class.py
from tests.fake_client import FakeClient
import unittest
from abc.base_class import BaseClass
import pytest
try:
import mock
except ImportError:
from unittest import mock
class TestBaseClassOperations(unittest.TestCase):
def setUp(self):
self.push_base = BaseClass("http://fake_host_nmae/test", "foo", "bar")
self.push_base.client = mock.patch('abc.base_class.Client', new=FakeClient()).start()
def test_create_valid_product(self):
product_dict = { # some stuff }
created_product_id = self.push_base.process_product(product_dict)
# process_product() will call create_product from fake client
# will return 1 if FakeClient().create_product() called
assert created_product_id == 1
I tried it another way.
#pytest.fixture
def fixture_product_creation():
return { # product fixture
}
#mock.patch('abc.base_class.Client', return_value=FakeClient())
class TestBaseClassAgain:
def test_create_valid_product(self, mock_client, fixture_product_creation):
push_base = BaseClass("http://fake_host_nmae/test", "foo", "bar")
created_product_id = push_base.process_product(fixture_product_creation)
expected = 1
assert created_product_id == expected
# how can I use this mock_client here?
Although I can replace the client with the FakeClient, but I am unsure how to arrange all the mock things to get it tested with the assert or assert_called_with calls.
I referred this but not able to arrange it in a proper pythonic way.
Can anyone help me rearrange this and suggest to me any better way to replace the client class with the fake client class by using pytest mock?
Thanks.
In order to properly test this you should make a change to your PushBase class. It is not good practice to instantiate a dependent object in the __init__ method of your class, instead consider passing the object in. This makes testing easier as you can just inject the dependency as needed. Another option would be to make a #classmethod that instantiates the object with the client. In the code below I illustrate how to do the former.
It also appears you have an indentation error as the update_product method can never be called based on the logic you currently have.
# base.py
class PushBase:
def __init__(self, client):
self.client = client
def process_product(self, item): # item type is dict {}
product_id = item.get('product_id')
if item.get('state') == 'present':
if not product_id:
# creating product
product_id = self.client.create_product(item.get('data'))
return product_id
# update product
self.client.update_product(item.get('data'))
elif item.get('state') == 'absent':
# delete product
self.client.delete_product(product_id)
# test_base.py
import pytest
from src.base import PushBase
def test_create_product_id(mocker):
mock_client = mocker.MagicMock()
base = PushBase(mock_client)
item = {
"state": "present",
"data": "fizz"
}
mock_client.create_product.return_value = "ok"
product_id = base.process_product(item)
assert product_id == "ok"
mock_client.create_product.assert_called_once_with("fizz")
def test_update_product(mocker):
mock_client = mocker.MagicMock()
base = PushBase(mock_client)
item = {
"state": "present",
"data": "bang",
"product_id": "baz"
}
base.process_product(item)
mock_client.update_product.assert_called_once_with("bang")
def test_delete_product(mocker):
mock_client = mocker.MagicMock()
base = PushBase(mock_client)
item = {
"state": "absent",
"product_id": "vroom"
}
base.process_product(item)
mock_client.delete_product.assert_called_once_with("vroom")
============================================== test session starts ===============================================
platform darwin -- Python 3.8.9, pytest-7.0.1, pluggy-1.0.0
rootdir: ***
plugins: asyncio-0.18.3, hypothesis-6.48.1, mock-3.7.0
asyncio: mode=strict
collected 3 items
tests/test_base.py ...
=============================================== 3 passed in 0.01s ================================================
I am using the pytest-mock package, which is where the mocker fixture comes from. The nice thing about being able to inject a dependency into your class is you don't need to configure all the methods beforehand, you can modify what you need within each test function. There are improvements you can make to the tests above, but that exercise is left to you. Hopefully this should help you understand the direction you should go in.
from fastapi import FastAPI
import person
app = FastAPI()
local_database = []
#app.get('/')
def index():
"""Return the todo"""
return "Use: URL/docs to access API documentation"
#app.get('/people')
def get_people():
if not local_database:
return 'Database empty'
people_info = [person for person in local_database]
return people_info
#app.post('/people')
def create_person(person: person.Person):
local_database.append(person)
return 'Added new person to database'
#app.get("/people/{person_id}")
def read_all_infos(person_id: int):
try:
return local_database[person_id]
except IndexError as e:
return repr(e)
# This is much better than any str()-like solutions, because it actually includes the type of exception.
#app.get("/people/information/salary")
def read_salary(name: str, gender: str, age: int, password: str):
for pers in local_database:
if ( pers.name == name
and pers.gender == gender
and password == "123456"
):
return pers.salary
return 'Could not find your person'
#app.get("/people/information/gender")
def read_gender(name: str):
print(name)
for pers in local_database:
if pers.name == name:
return pers
return 'Could not find your person'
# maybe needed
def populate_database():
dummy_person1 = person.Person(salary = 55000, gender="male", name ="Jack", age = 22)
dummy_person2 = person.Person(salary = 120000, gender="female", name ="Rose", age = 42)
local_database.append(dummy_person1)
local_database.append(dummy_person2)
if __name__ == '__main__':
populate_database()
My goal is to use FastAPI to communicate with a local in-memory database for testing purposes. However, in my main I'd like to run populate_database() to add some instances of the class Person to the list.
Then I want to use a HTTP request GET to receive the local_data.
Unfortunately the local_database list is not populated. I would expect 2 instances of Person inside the database.
Any idea why the local_database list is not populated?
If the module is run from standard input, a script, or from an interactive prompt its __name__ is being set as "__main__". 1
Effectively, populate_database is only executed when you're running it via python <filename> but you're probably running it using uvicorn or another runner which executes it as a normal module.
Try moving populate_database call outside the if __name__ = "__main__" guard.
#pytest.fixture(scope="session")
def create_user_access_token():
""" Used to test: refresh and revoke endpoints"""
response_data = oauth_requests.create_user_access_token()
return response_data["token"]
#pytest.fixture(scope="session")
def create_client_access_token():
""" Used to test: refresh and revoke endpoints"""
response_data = oauth_requests.create_client_access_token()
return response_data["token"]
#pytest.mark.parameterize('token', [create_client_access_token, create_user_access_token]
def test(token):
return_data = check(token)
assert return_data.status_code == 200
I understand it's not possible to do the above but how can I replicate this feature? The tests I am writing are identical for both tokens.
the fixtures are functions and request the API to create said token.
#mrBeanBremen Already started refactoring my code with the singleton pattern :). am leaving the function calls separate for flexibility. Thanks!
class Tokens:
__instance = None
def __new__(cls, user_token, client_token):
if cls.__instance is None:
cls.__instance = object.__new__(cls)
cls.__instance.user_token = user_token
cls.__instance.client_token = client_token
return cls.__instance
my project has a file called config.py which has, among others, the following code:
class Secret(Enum):
DATABASE_A = 'name_of_secret_database_A'
DATABASE_A = 'name_of_secret_database_A'
def secret(self):
if self.value:
return get_secret(self.value)
return {}
def get_secret(secret_name):
session = Session()
client = session.client(
service_name='secretsmanager',
region_name='us-east-1',
)
secret_value = client.get_secret_value(SecretId=secret_name)
return loads(secret_value.get('SecretString', "{}"))
I need to somehow mock get_secret in tests with pytest for all enum calls, for example Secret.DATABASE_A.secret ()
You can use monkeypatch to override the behaviour of get_secret(). I have made the get_secret() method a static method of the Secret class, but you can make it part of any module you want and import it as well. Just make sure you change in in the monkeypatch.setattr() call as well.
import pytest
from enum import Enum
class Secret(Enum):
DATABASE_A = 'name_of_secret_database_A'
DATABASE_B = 'name_of_secret_database_B'
def secret(self):
if self.value:
return Secret.get_secret(self.value)
return {}
#staticmethod
def get_secret(secret_name):
session = Session()
client = session.client(
service_name='secretsmanager',
region_name='us-east-1',
)
secret_value = client.get_secret_value(SecretId=secret_name)
return loads(secret_value.get('SecretString', "{}"))
def test_secret_method(monkeypatch):
def get_secret(secret_name):
return "supersecret"
monkeypatch.setattr(Secret, "get_secret", get_secret)
s = Secret.DATABASE_A
assert s.secret() == "supersecret"
This returns into 1 passed test.
What is happening here is, that I created a function get_secret() in my test_secret_method as well, and then overwrite the Secret.get_secret() with that new method. Now, you can use the Secret class in your test_method and be sure what the 'get_secret()' method will return without actually running the original code.
I have some trouble with integration test. I'm using python 3.5, SQLAlchemy 1.2.0b3, latest docker image of postgresql. So, I wrote test:
# tests/integration/usecases/test_users_usecase.py
class TestGetUsersUsecase(unittest.TestCase):
def setUp(self):
Base.metadata.reflect(_pg)
Base.metadata.drop_all(_pg)
Base.metadata.create_all(_pg)
self._session = sessionmaker(bind=_pg, autoflush=True, autocommit=False, expire_on_commit=True)
self.session = self._session()
self.session.add(User(id=1, username='user1'))
self.session.commit()
self.pg = PostgresService(session=self.session)
def test_get_user(self):
expected = User(id=1, username='user1')
boilerplates.get_user_usecase(storage_service=self.pg, id=1, expected=expected)
# tests/boilerplates/usecases/user_usecases.py
def get_user_usecase(storage_service, id, expected):
u = GetUser(storage_service=storage_service)
actual = u.apply(id=id)
assert expected == actual
In usecase I did next:
# usecases/users.py
class GetUser(object):
"""
Usecase for getting user from storage service by Id
"""
def __init__(self, storage_service):
self.storage_service = storage_service
def apply(self, id):
user = self.storage_service.get_user_by_id(id=id)
if user is None:
raise UserDoesNotExists('User with id=\'%s\' does not exists' % id)
return user
storage_service.get_user_by_id looks like:
# infrastructure/postgres.py (method of Postgres class)
def get_user_by_id(self, id):
from anna.domain.user import User
return self.session.query(User).filter(User.id == id).one_or_none()
And it does not work in my integration test. But if I add print(actual) in test before assert - all is OK. I thought that my test is bad, I try many variants and all does not works. Also I tried return generator from storage_service.get_user_by_id() and it also does not work. What I did wrong? It works good only if print() was called in test.