Unit tests mocks fails on rest API requests in Python - python

Hi I am new to unit testing and I am digging into mocks and pytest.I am trying to unit test two rest api requests where the GET request checks if an item doesn't exist in the API and if it does not create it with POST request and to create a folder, otherwise if it exists just to create a folder.
I tried to use Mocker() and I am stuck on AttributeError: Mocker when I try to mock the GET request.
This is the code I am trying to test:
client = requests.session()
# get item by name
response = client.get(
f"https://url.com/rest/item/info?name=test_item",
auth=username, password),
)
if (
response.status_code == 200
and response.json()["status"]
== "Item doesn't exist!"
):
logging.info(f"Creating item")
client.post(
"https://url.com/rest/item/create?name=test_item",
auth=username, password),
)
# directory creation
dir_make = "mkdir -p test_item/temperature"
exec_cmd(dir_make)
elif response.status_code == 200 and response.json()["status"]=="OK":
# directory creation
dir_make = "mkdir -p test_item/temperature"
exec_cmd(dir_make)
And this is the unit test that fails with AttributeError:
def test_existing_item(requests_mock, monkeypatch):
with requests_mock.Mocker() as mock:
mock.get("https://url.com/rest/item/info?name=test_item", text="OK")
resp = requests.get("https://url.com/rest/item/info?name=test_item")
assert resp.text == "OK"
EDIT: Test for item not found and POST mock. It seems like it doesn't add coverage to the else statement. How can be tested if the item exists and only the folder needs to be added in that case?
EDIT 2: Added elif statement instead of else and 2 separate tests, still the one test_existing_items() doesn't cover the elif statement...What am I doing wrong in that case?
def test_existing_item(monkeypatch):
with requests_mock.Mocker() as mock_request:
mock_request.get(requests_mock.ANY, text="success!")
resp = requests.get(
"https://url.com/rest/item/info?name=test_item",
auth=("mock_username", "mock_password"),
)
if resp.status_code == 200 and resp.json()["status"] == "OK":
dir_make = "mkdir -p test_item/temperature"
exec_cmd(dir_make)
encoded_auth = b64encode(b"mock_username:mock_password").decode("ascii")
assert mock_request.last_request.headers["Authorization"] == f"Basic {encoded_auth}"
def test_post_item(monkeypatch):
with requests_mock.Mocker() as mock_request:
mock_request.get(requests_mock.ANY, text="success!")
resp = requests.get(
"https://url.com/rest/item/info?name=test_item",
auth=("mock_username", "mock_password"),
)
if resp.status_code == 200 and resp.json()["status"] == "ERROR":
mock_request.get(requests_mock.ANY, text="success!")
requests.post(
"https://url.com/rest/item/create?name=test_item",
auth=("mock_username", "mock_password"),
)
dir_make = "mkdir -p test_item/temperature"
exec_cmd(dir_make)
encoded_auth = b64encode(b"mock_username:mock_password").decode("ascii")
assert mock_request.last_request.headers["Authorization"] == f"Basic {encoded_auth}"
I am not familiar with unit testing so any help would be appreciated to unit test this code.

import requests
import requests_mock
def test_existing_item(monkeypatch):
with requests_mock.Mocker() as mock:
mock.get("https://url.com/rest/item/info?name=test_item", text="OK")
resp = requests.get("https://url.com/rest/item/info?name=test_item")
assert resp.text == "OK"
Don't pass requests_mock as parameter and pytest should work fine.
EDIT:
As for your edit:
It seems like it doesn't add coverage to the else statement. How can be tested if the item exists and only the folder needs to be added in that case?
That would be because your if condition is always true, so it never accesses the code below the else statement. Your second question is rather unclear to me, but I believe you want to write several tests, one for your if statement and one for your else statement. As a rule of thumb, if you need conditional logic in your test, you have a problem: either everything should go as you want it to every time you run your tests, either you should abort and have the test fail, as you want your code to have the exact same behavior every time you run it - and your tests as well, by extension.

Related

How to mock client object

I am working on writing unittest for my fastapi project.
One endpoint includes getting a serviceNow ticket. Here is the code i want to test:
from aiosnow.models.table.declared import IncidentModel as Incident
from fastapi import APIRouter
router = APIRouter()
#router.post("/get_ticket")
async def snow_get_ticket(req: DialogflowRequest):
"""Retrieves the status of the ticket in the parameter."""
client = create_snow_client(
SNOW_TEST_CONFIG.servicenow_url, SNOW_TEST_CONFIG.user, SNOW_TEST_CONFIG.pwd
)
params: dict = req.sessionInfo["parameters"]
ticket_num = params["ticket_num"]
try:
async with Incident(client, table_name="incident") as incident:
response = await incident.get_one(Incident.number == ticket_num)
stage_value = response.data["state"].value
desc = response.data["description"]
[...data manipulation, unimportant parts]
What i am having trouble with is trying to mock the client response, every time the actual client gets invoked and it makes the API call which i dont want.
Here is the current version of my unittest:
from fastapi.testclient import TestClient
client = TestClient(app)
#patch("aiosnow.models.table.declared.IncidentModel")
def test_get_ticket_endpoint_valid_ticket_num(self, mock_client):
mock_client.return_value = {"data" : {"state": "new",
"description": "test"}}
response = client.post(
"/snow/get_ticket", json=json.load(self.test_request)
)
assert response.status_code == 200
I think my problem is patching the wrong object, but i am not sure what else to patch.
In your test your calling client.post(...) if you don't want this to go to the Service Now API this client should be mocked.
Edit 1:
Okay so the way your test is setup now the self arg is the mocked IncidentModel object. So only this object will be a mock. Since you are creating a brand new IncidentModel object in your post method it is a real IncidentModel object, hence why its actually calling the api.
In order to mock the IncidentModel.get_one method so that it will return your mock value any time an object calls it you want to do something like this:
def test_get_ticket_endpoint_valid_ticket_num(mock_client):
mock_client.return_value = {"data" : {"state": "new",
"description": "test"}}
with patch.object(aiosnow.models.table.declared.IncidentModel, "get_one", return_value=mock_client):
response = client.post(
"/snow/get_ticket", json=json.load(self.test_request)
)
assert response.status_code == 200
The way variable assignment works in python, changing aiosnow.models.table.declared.IncidentModel will not change the IncidentModel that you've imported into your python file. You have to do the mocking where you use the object.
So instead of #patch("aiosnow.models.table.declared.IncidentModel"), you want to do #patch("your_python_file.IncidentModel")

How to create a unit test in Python for specific code in pytest

I have the following function:
def update_installation_register(
remote: RemoteRegisterData, install_id: str, data: dict, verify: bool
) -> None:
"""Updates an Installation register"""
credentials = (remote.auth_user, remote.auth_pass)
url = f"https://{remote.hostname}/api/instalaciones/{install_id}/"
resp = requests.put(url=url, auth=credentials, json=data, verify=verify)
if resp.status_code != 200:
raise RemoteRegisterRequestError()
It's part of a existing codebase, and I'm trying to add unit test to it.
I am not figuring out how to create a simple test for it, maybe a test that checks return None if all goes well and to catch an exception if it does not?
I'm not sure how to mock that request, if I write a test_update_installation_register for pytest
I am willing to refactor the code if necessary, although I'd prefer to leave it the way it is,
I'm puzzled by how this simple function managed to confuse me this much, but there it is.
The thing is, and maybe this is the fundamental question... How do I test a function that is merely a simple wrapper to a http request, without actually testing the request?
This is the best I could come up with, but I'm not entirely sure this is the right approach
def test_update_installation_register_valid():
with requests_mock.Mocker() as mock:
remote_register = remote_register_data()
mock.put(
f"https://{remote_register.hostname}/api/instalaciones/{req_test_id()}/",
status_code=200,
)
resp = update_installation_register(
remote_register, req_test_id(), req_test_data(), verify=True
)
assert resp == None
where
remote_register_data()
req_test_id()
req_test_id()
req_test_data()
are just returning a hardcoded "test" value
Is this a good solution?

Testing Flask Sessions with Pytest

Currently I'm working on a Flask project and need to make some tests.
The test I'm struggling is about Flask Sessions.
I have this view:
#blue_blueprint.route('/dashboard')
"""Invoke dashboard view."""
if 'expires' in session:
if session['expires'] > time.time():
pass
else:
refresh_token()
pass
total_day = revenues_day()
total_month = revenues_month()
total_year = revenues_year()
total_stock_size = stock_size()
total_stock_value = stock_value()
mean_cost = total_stock_value/total_stock_size
return render_template('dashboard.html.j2', total_day=total_day, <br> total_month=total_month, total_year=total_year, total_stock_size=total_stock_size, total_stock_value=total_stock_value, mean_cost=mean_cost)
else:
return redirect(url_for('blue._authorization'))
And have this test:
def test_dashboard(client):
with client.session_transaction(subdomain='blue') as session:
session['expires'] = time.time() + 10000
response = client.get('/dashboard', subdomain='blue')
assert response.status_code == 200
My currently conftest.py is:
#pytest.fixture
def app():
app = create_app('config_testing.py')
yield app
#pytest.fixture
def client(app):
return app.test_client(allow_subdomain_redirects=True)
#pytest.fixture
def runner(app):
return app.test_cli_runner(allow_subdomain_redirects=True)
However, when I execute the test, I'm getting a 302 status code instead of the expected 200 status code.
So my question is how I can pass properly the session value?
OBS: Running normally the application the if statement for session is working properly.
I find the solution and I want to share with you the answer.
In the API documentation Test Client says:
When used in combination with a with statement this opens a session transaction. This can be used to modify the session that the test client uses. Once the with block is left the session is stored back.
We should put the assert after with statement not in, for this work, so the code should be:
def test_dashboard(client):
with client.session_transaction(subdomain='blue') as session:
session['expires'] = time.time() + 10000
response = client.get('/dashboard', subdomain='blue')
assert response.status_code == 200
This simple indent solves my problem.

Catch http-status code in Flask

I lately started using Flask in one of my projects to provide data via a simple route. So far I return a json file containing the data and some other information. When running my Flask app I see the status code of this request in terminal. I would like to return the status code as a part of my final json file. Is it possible to catch the same code I see in terminal?
Some simple might look like this
from flask import Flask
from flask import jsonify
app = Flask(__name__)
#app.route('/test/<int1>/<int2>/')
def test(int1,int2):
int_sum = int1 + int2
return jsonify({"result":int_sum})
if __name__ == '__main__':
app.run(port=8082)
And in terminal I get:
You are who set the response code (by default 200 on success response), you can't catch this value before the response is emited. But if you know the result of your operation you can put it on the final json.
#app.route('/test/<int1>/<int2>/')
def test(int1, int2):
int_sum = int1 + int2
response_data = {
"result": int_sum,
"sucess": True,
"status_code": 200
}
# make sure the status_code on your json and on the return match.
return jsonify(response_data), 200 # <- the status_code displayed code on console
By the way if you access this endpoint from a request library, on the response object you can find the status_code and all the http refered data plus the json you need.
Python requests library example
import requests
req = requests.get('your.domain/test/3/3')
print req.url # your.domain/test/3/3
print req.status_code # 200
print req.json() # {u'result': 6, u'status_code: 200, u'success': True}
You can send HTTP status code as follow:
#app.route('/test')
def test():
status_code = 200
return jsonify({'name': 'Nabin Khadka'}, status_code) # Notice second element of the return tuple(return)
This way you can control what status code to return to the client (typically to web browser.)

Setting Properties on Mock Not Working

I'm trying to create a Mock of a library's (Hammock) call to a POST method that has an attribute status_code. Here is my test code:
def test_send_text(self):
Hammock.POST = Mock(status_code=201)
print Hammock.POST.status_code
self.task.payload = fixtures.text_payload
self.task.send_text()
# ········
Hammock.POST.assert_any_call()
When I print Hammock.POST.status_code, I get what I expect -- 201. However, when we move into the code I'm testing:
response = self.twilio_api('Messages.json').POST(data=self.payload)
print '*' * 10
print response
print response.status_code
if response.status_code == 201:
self.logger.info('Text message successfully sent.')
else:
raise NotificationDispatchError('Twilio request failed. {}. {}'.format(response.status_code,
response.content))
Things get weird. response is, indeed, a Mock object. But response.status_code, instead of being 201 like when I try it in the test, is an object: <Mock name='mock().status_code' id='4374061968'>. Why is my mocked attribute working in the test code and not in the code that I'm testing?
The issue is POST().status_code vs POST.status_code. POST.status_code will indeed == 201, but the return object from POST() is a completely new mock object.
What you are looking for is roughly
Hammock.POST = Mock()
Hammock.POST.return_value = Mock(status_code=201)

Categories

Resources