Currently I'm writing a unite testing for my flask project. I wrote a function to test the login feature. When I run the unit test, it showed some error message.
FAILED unit_test.py::TestClass::test_login - AttributeError: 'WrapperTestResponse' object has no attribute 'text'
2.Here is my code for the unit testing implementation, I can get the status code successfully but not the text. Did I make some mistakes?
import unittest
from app import app
import requests
from flask import request
import json
class TestClass(unittest.TestCase):
def setup_class(self):
app.config['TESTING'] = True
self.app = app.test_client()
def teardown_class(self):
"""Do the testing """
pass
def test_login(self):
response = self.app.get('/login')
print(response)
data = {'username': '123456#qq.com', 'password': '12345678'}
response = app.test_client().post('/login', data=json.dumps(data))
self.assertEqual(response.status_code, 200)
print('--------------')
self.assertEqual(response.text, "Invalid login credentials")
I think you are looking for response.data instead
A descriptor that calls get_data() and set_data().
get_data gives
The string representation of the response body...
Example output if your view function returns 'Invalid login credentials':
>>> response.data
b'Invalid login credentials'
Related
I am only beginner in flask. Trying to integrate marshmallow and webargs. It perfectly works in flask-restful Resource class. But when I use a simple flask route it does not work
routes.py
class UserAPI(Resource):
#use_args(UserSchema())
def post(self, *args):
print(args)
return 'success', 201
def get(self):
return '<h1>Hello</h1>'
#bp.route('/test/', methods=['POST'])
#use_kwargs(UserSchema())
def test2(*args, **kwargs):
print(args)
print(kwargs)
return 'success', 201
api.add_resource(UserAPI, '/', endpoint='user')
I've added error handler which is necessary when using use_args
from webargs.flaskparser import parser, abort
from webargs import core
#parser.error_handler
def webargs_validation_handler(error, req, schema, *, error_status_code, error_headers):
status_code = error_status_code or core.DEFAULT_VALIDATION_STATUS
abort(
400,
exc=error,
messages=error.messages,
)
That's what I'm getting when I make request to Resource endpoint what is normal
And that's what I'm getting when I make request to a simple flask route what is not normal
I want to be able to use both ways
Found answer in webargs docs :)
https://webargs.readthedocs.io/en/latest/framework_support.html#error-handling
from flask import jsonify
# Return validation errors as JSON
#app.errorhandler(422)
#app.errorhandler(400)
def handle_error(err):
headers = err.data.get("headers", None)
messages = err.data.get("messages", ["Invalid request."])
if headers:
return jsonify({"errors": messages}), err.code, headers
else:
return jsonify({"errors": messages}), err.code
I do not know what is wrong with this code when I am trying to test login, first assert passed so user created successfully but failed in second assert.
Hint: I am using simple JWT
Test Code:
from user.models import User
from rest_framework.test import APIClient
from mixer.backend.django import mixer
from django.urls import reverse
import pytest
client = APIClient()
#pytest.mark.django_db
class TestUser():
#pytest.fixture
def setup_user(self):
user = mixer.blend(User, email='a#a.com', password='1')
return user
def test_login(self, setup_user):
assert User.objects.count() == 1
data = {'email': 'a#a.com', 'password': '1'}
response = client.post(reverse('login'), data=data)
assert response.status_code == 200
URL:
path('api/login/', TokenObtainPairView.as_view(), name='login'),
ERROR:
FAILED user/tests/test_user.py::TestUser::test_login - assert 401 == 200
Just for the record: You need to set the password correctly to be able to hash it, in order for the login to work:
#pytest.fixture
def setup_user(self):
user = mixer.blend(User, email='a#a.com')
user.set_password('1')
user.save()
return user
try adding format="json" to your client post call
Title might be a little confusing.
Say I have an APIView with a post method. Inside the post method, I introduced a class that has its own method. In this case, it's a class that deals with uploading to S3, which is something I want to skip when running unittest.
class SomeView(APIView):
def post(self):
# do something here
input1 = some_process(payload_arg1)
input2 = some_other_process(payload_arg2)
uploader = S3Uploader()
s3_response = uploader.upload_with_aux_fxn(input1, input2)
if s3_response['status_code'] == 200:
# do something else
return Response('Good job I did it!', status_code=200)
else:
return Response('noooo you're horrible!', status_code=400)
Real code has different function calls and responses, obviously.
Now I need to mock that uploader and uploader.upload_with_aux_fxn so I don't actually call S3. How do I mock it?
I tried in my test script
from some_place import S3Uploader
class SomeViewTestCase(TestCase):
def setUp(self):
self.client = APIClient()
uploader_mock = S3Uploader()
uploader_mock.upload_support_doc = MagicMock(return_value={'status_code': 200, 'message': 'asdasdad'}
response = self.client.post(url, payload, format='multipart')
But I still triggered S3 upload (as file shows up in S3). How do I correctly mock this?
EDIT1:
My attempt to patch
def setUp(self):
self.factory = APIRequestFactory()
self.view = ViewToTest.as_view()
self.url = reverse('some_url')
#patch('some_place.S3Uploader', FakeUploader)
def test_uplaod(self):
payload = {'some': 'data', 'other': 'stuff'}
request = self.factory.post(self.url, payload, format='json')
force_authenticate(request, user=self.user)
response = self.view(request)
where the FakeUplaoder is
class FakeUplaoder(object):
def __init__(self):
pass
def upload_something(self, data, arg1, arg2, arg3):
return {'status_code': 200, 'message': 'unit test', 's3_path':
'unit/test/path.pdf'}
def downlaod_something(self, s3_path):
return {'status_code': 200, 'message': '', 'body': 'some base64
stuff'}
unfortunately this is not successful. I still hit the actual class
EDIT 2:
I'm using Django 1.11 and Python 2.7, in case people need this info
I guess the correct approach to it would be save the file within a model with FileField, and then connect Boto to handle upload in production scenario.
Take a good look at:
https://docs.djangoproject.com/en/2.2/ref/models/fields/#filefield
and
https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#model
this approach would preserve Django default behavior, making things more testable with Django's default test client.
Take a look at vcrpy. It records request to external API once and then replays answer every time you run your tests. No need to manually mock anything.
Here's an example of how I would mock that S3Uploader in an APITestCase.
from rest_framework import status
from unittest import mock
from unittest.mock import MagicMock
class SomeViewTestCase(APITestCase):
#mock.patch("path.to.view_file.S3Uploader")
def test_upload(self, s3_uploader_mock):
"""Test with mocked S3Uploader"""
concrete_uploader_mock = MagicMock(**{
"upload_with_aux_fxn__return_value": {"status_code": 200}
})
s3_uploader_mock.return_value = concrete_uploader_mock
response = self.client.post(url, payload, format='multipart')
self.assertEqual(response.status_code, status.HTTP_200_OK)
s3_uploader_mock.assert_called_once()
concrete_uploader_mock.upload_with_aux_fx.assert_called_once()
Try using MagicMock like below
from unittest import mock
from storages.backends.s3boto3 import S3Boto3Storage
class SomeTestCase(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.view = ViewToTest.as_view()
self.url = reverse('some_url')
#mock.patch.object(S3Boto3Storage, '_save', MagicMock(return_value='/tmp/somefile.png'))
def test_uplaod(self):
payload = {'some': 'data', 'other': 'stuff'}
request = self.factory.post(self.url, payload, format='json')
force_authenticate(request, user=self.user)
response = self.view(request)
I am trying to write unit test cases for flas api server.
Can someeone please suggest ow to get rid of auth.login_required.
Tried mocking auth, but of no use.
with test_client its not hitting code block too.
api.py
from flask import Flask
from flask.ext.httpauth import HTTPBasicAuth
app = Flask(__name__)
auth = HTTPBasicAuth()
#app.route('/')
#auth.login_required
def index():
print "In index"
response.status_code = 200
return response
Tried following http://flask.pocoo.org/docs/0.12/testing/
from src.api import app
from unittest import TestCase
class TestIntegrations(TestCase):
def setUp(self):
self.app = app.test_client()
def test_thing(self):
response = self.app.get('/')
Can someone please help ??
There are two ways to do so - first is to disable authorization in tests:
// in your test module
from api import app, auth
import unittest
#auth.verify_password
def verify_password(user, password):
"""Overwrite password check to always pass.
This works even if we send no auth data."""
return True
Another approach is to actually send the auth headers from tests (this way you can also test your authorization system):
from api import app
from base64 import b64encode
import unittest
class ApiClient:
"""Performs API requests."""
def __init__(self, app):
self.client = app.test_client()
def get(self, url, **kwargs):
"""Sends GET request and returns the response."""
return self.client.get(url, headers=self.request_headers(), **kwargs)
def request_headers(self):
"""Returns API request headers."""
auth = '{0}:{1}'.format('user', 'secret')
return {
'Accept': 'application/json',
'Authorization': 'Basic {encoded_login}'.format(
encoded_login=b64encode(auth.encode('utf-8')).decode('utf-8')
)
}
class TestIntegrations(unittest.TestCase):
def setUp(self):
self.app = ApiClient(app)
def test_thing(self):
response = self.app.get('/')
print(response.data)
The ApiClient helper can also define post, delete methods which will be similar to get.
The full source code with examples is here.
The following test code does not pass even though manually submitting the form on my web interface actually does work.
import os
from flask.ext.testing import TestCase
from flask import url_for
from config import _basedir
from app import app, db
from app.users.models import User
class TestUser(TestCase):
def create_app(self):
"""
Required method. Always implement this so that app is returned with context.
"""
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(_basedir, 'test.db')
app.config['WTF_CSRF_ENABLED'] = False # This must be disabled for post to succeed during tests
self.client = app.test_client()
ctx = app.app_context()
ctx.push()
return app
def setUp(self):
db.create_all()
#pass
#app.teardown_appcontext
def tearDown(self):
db.session.remove()
db.drop_all()
#pass
def test_admin_home(self):
# url is the admin home page
url = url_for('admin.index')
resp = self.client.get(url)
self.assertTrue(resp.status_code == 200)
def test_admin_registration(self):
url = url_for('admin.register_view')
data = {'username': 'admin', 'email': 'admin#example.com', 'password': 'admin'}
resp = self.client.post(url, data)
self.assertTrue(resp.status_code == 200)
u = User.query.filter_by(username=u'admin').first()
self.assertTrue(u.username == 'admin') # <----- This fails. Why?
After the test client has post to the register_view url and returns a 200 OK response, I fail to retrieve the 'admin' user from the test database. Why is this so?
Here's the view code (this is a flask admin view)
from flask import request
from flask.ext.admin import expose, AdminIndexView, helpers
from app.auth.forms import LoginForm, RegistrationForm
from app.users.models import User
from app import db
class MyAdminIndexView(AdminIndexView):
#expose('/', methods=('GET', 'POST'))
def index(self):
# handle user login
form = LoginForm(request.form)
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
#expose('/register/', methods=('GET', 'POST'))
def register_view(self):
# handle user registration
form = RegistrationForm(request.form)
if helpers.validate_form_on_submit(form):
user = User()
form.populate_obj(user)
db.session.add(user)
db.session.commit()
self._template_args['form'] = form
return super(MyAdminIndexView, self).index()
Dumbest mistake ever.
The offending line in my test code is
resp = self.client.post(url, data)
It should be
resp = self.client.post(url, data=data)
I managed to track it down by painstakingly walking through the logic and inserting ipdb.set_trace() step by step until I found the bad POST request made by my client.